mirror of
https://github.com/mikedilger/chorus.git
synced 2026-08-01 07:21:39 +00:00
MAJOR switch to pocket
This commit is contained in:
parent
3c9c442bb5
commit
02de9e2038
401
Cargo.lock
generated
401
Cargo.lock
generated
@ -19,24 +19,24 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.69"
|
||||
version = "0.3.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
|
||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
@ -61,9 +61,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 = "bincode"
|
||||
@ -92,15 +92,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@ -114,12 +108,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@ -128,18 +116,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.83"
|
||||
version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -151,7 +136,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
name = "chorus"
|
||||
version = "1.3.0"
|
||||
dependencies = [
|
||||
"chorus-lib",
|
||||
"base64 0.22.1",
|
||||
"dashmap",
|
||||
"env_logger",
|
||||
"futures",
|
||||
@ -160,8 +145,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pocket-db",
|
||||
"pocket-types",
|
||||
"rustls-pemfile",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
"speedy",
|
||||
"tempfile",
|
||||
"textnonce",
|
||||
"tokio",
|
||||
@ -171,25 +160,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chorus-lib"
|
||||
version = "1.3.0"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"futures",
|
||||
"heed",
|
||||
"hyper",
|
||||
"hyper-tungstenite",
|
||||
"log",
|
||||
"mmap-append",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
"speedy",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
@ -240,9 +214,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
@ -294,9 +281,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
@ -369,7 +356,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -425,9 +412,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.12"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@ -442,17 +429,16 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.3"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heed"
|
||||
version = "0.20.0-alpha.8"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=64fd6fec293c0dee94855b8267557ce03e7ce5d8#64fd6fec293c0dee94855b8267557ce03e7ce5d8"
|
||||
version = "0.20.0-alpha.9"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bytemuck",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"heed-types",
|
||||
@ -467,16 +453,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "heed-traits"
|
||||
version = "0.20.0-alpha.8"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=64fd6fec293c0dee94855b8267557ce03e7ce5d8#64fd6fec293c0dee94855b8267557ce03e7ce5d8"
|
||||
version = "0.20.0-alpha.9"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
|
||||
[[package]]
|
||||
name = "heed-types"
|
||||
version = "0.20.0-alpha.8"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=64fd6fec293c0dee94855b8267557ce03e7ce5d8#64fd6fec293c0dee94855b8267557ce03e7ce5d8"
|
||||
version = "0.20.0-alpha.9"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"serde",
|
||||
@ -485,9 +470,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.6"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
@ -497,9 +482,9 @@ checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.11"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@ -583,9 +568,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.3"
|
||||
version = "2.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@ -604,9 +589,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
@ -616,9 +601,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
@ -629,7 +614,7 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||
[[package]]
|
||||
name = "lmdb-master-sys"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=64fd6fec293c0dee94855b8267557ce03e7ce5d8#64fd6fec293c0dee94855b8267557ce03e7ce5d8"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"doxygen-rs",
|
||||
@ -638,9 +623,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
@ -648,15 +633,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.1"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
@ -669,9 +654,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
@ -687,9 +672,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
@ -733,9 +718,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
@ -743,9 +728,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
@ -753,15 +738,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.9"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -800,7 +785,7 @@ dependencies = [
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -814,9 +799,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@ -824,6 +809,25 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pocket-db"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mikedilger/pocket?branch=master#257d096fb3ed2347f30b6c0b93346dff6f8ee395"
|
||||
dependencies = [
|
||||
"heed",
|
||||
"mmap-append",
|
||||
"pocket-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pocket-types"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mikedilger/pocket?branch=master#257d096fb3ed2347f30b6c0b93346dff6f8ee395"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"secp256k1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@ -832,18 +836,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -907,7 +911,7 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.12",
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -921,18 +925,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.3"
|
||||
version = "1.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -942,9 +946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -953,9 +957,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
@ -965,7 +969,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.12",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
@ -979,12 +983,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.31"
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@ -993,9 +1006,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.10"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
@ -1065,30 +1078,36 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
name = "semver"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.200"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
version = "1.0.200"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.114"
|
||||
version = "1.0.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -1117,9 +1136,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@ -1141,18 +1160,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.1"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.5"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1173,7 +1192,7 @@ checksum = "658f2ca5276b92c3dfd65fa88316b4e032ace68f88d7570b43967784c0bac5ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1184,9 +1203,20 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.50"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1204,9 +1234,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.0"
|
||||
version = "3.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
@ -1235,22 +1265,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.57"
|
||||
version = "1.0.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
|
||||
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.57"
|
||||
version = "1.0.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1270,9 +1300,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.36.0"
|
||||
version = "1.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@ -1295,7 +1325,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1322,9 +1352,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.10"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
|
||||
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@ -1343,9 +1373,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.6"
|
||||
version = "0.22.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
|
||||
checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@ -1424,9 +1454,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
@ -1499,11 +1529,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
|
||||
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1527,7 +1557,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1547,17 +1577,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.0",
|
||||
"windows_aarch64_msvc 0.52.0",
|
||||
"windows_i686_gnu 0.52.0",
|
||||
"windows_i686_msvc 0.52.0",
|
||||
"windows_x86_64_gnu 0.52.0",
|
||||
"windows_x86_64_gnullvm 0.52.0",
|
||||
"windows_x86_64_msvc 0.52.0",
|
||||
"windows_aarch64_gnullvm 0.52.5",
|
||||
"windows_aarch64_msvc 0.52.5",
|
||||
"windows_i686_gnu 0.52.5",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.5",
|
||||
"windows_x86_64_gnu 0.52.5",
|
||||
"windows_x86_64_gnullvm 0.52.5",
|
||||
"windows_x86_64_msvc 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1568,9 +1599,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
@ -1580,9 +1611,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
@ -1592,9 +1623,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
@ -1604,9 +1641,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
@ -1616,9 +1653,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
@ -1628,9 +1665,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
@ -1640,15 +1677,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.0"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.2"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178"
|
||||
checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
39
Cargo.toml
39
Cargo.toml
@ -1,6 +1,33 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"chorus-lib",
|
||||
"chorus-bin",
|
||||
]
|
||||
resolver = "2"
|
||||
[package]
|
||||
name = "chorus"
|
||||
version = "1.3.0"
|
||||
description = "A personal relay for nostr"
|
||||
authors = ["Mike Dilger <mike@mikedilger.com>"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/mikedilger/chorus"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22"
|
||||
dashmap = "5.5"
|
||||
env_logger = "0.10"
|
||||
futures = "0.3"
|
||||
hyper = { version = "0.14", features = [ "http1", "server", "runtime", "stream" ] }
|
||||
hyper-tungstenite = "0.11"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
pocket-types = { git = "https://github.com/mikedilger/pocket", branch = "master" }
|
||||
pocket-db = { git = "https://github.com/mikedilger/pocket", branch = "master" }
|
||||
parking_lot = "0.12"
|
||||
rustls-pemfile = "1.0"
|
||||
secp256k1 = { version = "0.28", features = [ "hashes", "global-context", "rand-std" ] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
speedy = "0.8"
|
||||
textnonce = "1"
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tokio-rustls = "0.24"
|
||||
toml = "0.8"
|
||||
url = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@ -1,29 +0,0 @@
|
||||
[package]
|
||||
name = "chorus"
|
||||
version = "1.3.0"
|
||||
description = "A personal relay for nostr"
|
||||
authors = ["Mike Dilger <mike@mikedilger.com>"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/mikedilger/chorus"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chorus-lib = { path = "../chorus-lib" }
|
||||
dashmap = "5.5"
|
||||
env_logger = "0.10"
|
||||
futures = "0.3"
|
||||
hyper = { version = "0.14", features = [ "http1", "server", "runtime", "stream" ] }
|
||||
hyper-tungstenite = "0.11"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
parking_lot = "0.12"
|
||||
rustls-pemfile = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
textnonce = "1"
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tokio-rustls = "0.24"
|
||||
toml = "0.8"
|
||||
url = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@ -1,38 +0,0 @@
|
||||
use chorus_lib::config::{Config, FriendlyConfig};
|
||||
use chorus_lib::error::Error;
|
||||
use chorus_lib::store::Store;
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus_compress <config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
// Read config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let config: Config = friendly_config.into_config()?;
|
||||
|
||||
env_logger::Builder::new()
|
||||
.filter_level(config.library_log_level)
|
||||
.filter(Some("Server"), config.server_log_level)
|
||||
.filter(Some("Client"), config.client_log_level)
|
||||
.format_target(true)
|
||||
.format_module_path(false)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
log::debug!(target: "Server", "Loaded config file.");
|
||||
|
||||
Store::rebuild(&config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
use chorus_lib::config::{Config, FriendlyConfig};
|
||||
use chorus_lib::error::Error;
|
||||
use chorus_lib::store::Store;
|
||||
use chorus_lib::types::{Event, Filter};
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus_dump <chorus_config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
// Read config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let mut config: Config = friendly_config.into_config()?;
|
||||
|
||||
env_logger::Builder::new()
|
||||
.filter_level(config.library_log_level)
|
||||
.filter(Some("Server"), config.server_log_level)
|
||||
.filter(Some("Client"), config.client_log_level)
|
||||
.format_target(true)
|
||||
.format_module_path(false)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
log::debug!(target: "Server", "Loaded config file.");
|
||||
|
||||
// Force allow of scraping (this program is a scraper)
|
||||
config.allow_scraping = true;
|
||||
|
||||
// Setup store
|
||||
let store = Store::new(&config)?;
|
||||
|
||||
let mut buffer: [u8; 128] = [0; 128];
|
||||
let (_incount, _outcount, filter) = Filter::from_json(b"{}", &mut buffer)?;
|
||||
let screen = |_: &Event<'_>| -> bool { true };
|
||||
|
||||
let mut events = store.find_events(filter, screen, &config)?;
|
||||
for event in events.drain(..) {
|
||||
let bytes = event.as_json()?;
|
||||
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
println!("{s}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
use chorus_lib::config::{Config, FriendlyConfig};
|
||||
use chorus_lib::error::Error;
|
||||
use chorus_lib::store::Store;
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus_moderate <config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
// Read config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let mut config: Config = friendly_config.into_config()?;
|
||||
|
||||
env_logger::Builder::new()
|
||||
.filter_level(config.library_log_level)
|
||||
.filter(Some("Server"), config.server_log_level)
|
||||
.filter(Some("Client"), config.client_log_level)
|
||||
.format_target(true)
|
||||
.format_module_path(false)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
log::debug!(target: "Server", "Loaded config file.");
|
||||
|
||||
// Force allow of scraping (this program is a scraper)
|
||||
config.allow_scraping = true;
|
||||
|
||||
// Setup store
|
||||
let store = Store::new(&config)?;
|
||||
|
||||
for (id, approved) in store.dump_event_approvals()? {
|
||||
println!("ID {} = {}", id, approved);
|
||||
}
|
||||
|
||||
for (pubkey, approved) in store.dump_pubkey_approvals()? {
|
||||
println!("PUBKEY {} = {}", pubkey, approved);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "chorus-lib"
|
||||
version = "1.3.0"
|
||||
description = "A personal relay for nostr"
|
||||
authors = ["Mike Dilger <mike@mikedilger.com>"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/mikedilger/chorus"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22"
|
||||
futures = "0.3"
|
||||
heed = { git = "https://github.com/meilisearch/heed", rev = "64fd6fec293c0dee94855b8267557ce03e7ce5d8" }
|
||||
hyper = { version = "0.14", features = [ "http1", "server", "runtime", "stream" ] }
|
||||
hyper-tungstenite = "0.11"
|
||||
log = "0.4"
|
||||
mmap-append = "0.2"
|
||||
secp256k1 = { version = "0.28", features = [ "hashes", "global-context", "rand-std" ] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
speedy = "0.8"
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tokio-rustls = "0.24"
|
||||
toml = "0.8"
|
||||
url = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@ -1,8 +0,0 @@
|
||||
include!("macros.rs");
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod ip;
|
||||
pub mod reply;
|
||||
pub mod store;
|
||||
pub mod types;
|
||||
@ -1,75 +0,0 @@
|
||||
static HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
|
||||
#[allow(clippy::zero_prefixed_literal)]
|
||||
static HEX_INVERSE: [u8; 128] = {
|
||||
const __: u8 = 255;
|
||||
[
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0
|
||||
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 1
|
||||
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2
|
||||
00, 01, 02, 03, 04, 05, 06, 07, 08, 09, __, __, __, __, __, __, // 3
|
||||
__, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, // 4
|
||||
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 5
|
||||
__, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, // 6
|
||||
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7
|
||||
]
|
||||
};
|
||||
|
||||
macro_rules! write_hex {
|
||||
($input:expr, $output:expr, $bytelen:expr) => {{
|
||||
assert_eq!($input.len(), $bytelen);
|
||||
if $output.len() != $bytelen * 2 {
|
||||
Err(crate::error::ChorusError::BufferTooSmall.into())
|
||||
} else {
|
||||
for (i, byte) in $input.iter().enumerate() {
|
||||
$output[i * 2] = crate::HEX_CHARS[((byte & 0xF0) >> 4) as usize];
|
||||
$output[i * 2 + 1] = crate::HEX_CHARS[(byte & 0x0F) as usize];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! read_hex {
|
||||
($input:expr, $output:expr, $bytelen:expr) => {{
|
||||
assert_eq!($output.len(), $bytelen);
|
||||
if $input.len() != $bytelen * 2 {
|
||||
Err(Into::<crate::error::Error>::into(crate::error::ChorusError::EndOfInput))
|
||||
} else {
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let high = crate::HEX_INVERSE[$input[i * 2] as usize];
|
||||
if high == 255 {
|
||||
break Err(crate::error::ChorusError::BadHexInput.into());
|
||||
}
|
||||
let low = crate::HEX_INVERSE[$input[i * 2 + 1] as usize];
|
||||
if low == 255 {
|
||||
break Err(crate::error::ChorusError::BadHexInput.into());
|
||||
}
|
||||
$output[i] = high * 16 + low;
|
||||
i += 1;
|
||||
if i == $bytelen {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_u16 {
|
||||
($input:expr, $start:expr) => {
|
||||
u16::from_ne_bytes($input[$start .. $start+2].try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! parse_u32 {
|
||||
($input:expr, $start:expr) => {
|
||||
u32::from_ne_bytes($input[$start .. $start+4].try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! parse_u64 {
|
||||
($input:expr, $start:expr) => {
|
||||
u64::from_ne_bytes($input[$start .. $start+8].try_into().unwrap())
|
||||
}
|
||||
}
|
||||
@ -1,245 +0,0 @@
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::types::Event;
|
||||
use mmap_append::MmapAppend;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::mem;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// This is the size of the initial event map, and also how large it grows by
|
||||
/// when we need to grow it. This should be a multiple of the page size (4096)
|
||||
// While debugging, we like to use a small value to increase the frequency of
|
||||
// resizing, to help detect if there are problems in the algorithm.
|
||||
#[cfg(debug_assertions)]
|
||||
const EVENT_MAP_CHUNK: usize = 2048;
|
||||
#[cfg(not(debug_assertions))]
|
||||
const EVENT_MAP_CHUNK: usize = 4096 * 1024; // grow by 4 megabytes at a time
|
||||
|
||||
/// An EventStore is a fast storage facility for events.
|
||||
#[derive(Debug)]
|
||||
pub struct EventStore {
|
||||
// the Mmap doesn't need us to keep the file, but we keep it for resizing.
|
||||
event_map_file: File,
|
||||
event_map_file_len: AtomicUsize,
|
||||
|
||||
// This is a linear sequence of events in an append-only memory mapped file which
|
||||
// internally remembers the 'end' pointer and internally prevents multiple writers.
|
||||
event_map: MmapAppend,
|
||||
|
||||
// Whether or not event offsets are aligned on 8-byte boundaries. If they are,
|
||||
// event retrieval is more CPU efficient, and a tiny bit more space is used.
|
||||
// Early code did not align the data. If you specify the wrong value here that
|
||||
// doesnt match the data, the iterator will fail badly.
|
||||
aligned: bool,
|
||||
}
|
||||
|
||||
impl EventStore {
|
||||
/// Create a new `EventStore`. The `event_map_file` is the eventually large file
|
||||
/// that holds all the events.
|
||||
pub fn new<P: AsRef<Path>>(event_map_file: P, aligned: bool) -> Result<EventStore, Error> {
|
||||
// Open the event map file, possibly creating if it isn't there
|
||||
let event_map_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.create(true)
|
||||
.open(event_map_file)?;
|
||||
|
||||
// Get it's size
|
||||
let metadata = event_map_file.metadata()?;
|
||||
let mut len = metadata.len() as usize;
|
||||
|
||||
// Determine if we just created it
|
||||
// (not long enough for the required end offset)
|
||||
let new = len < mem::size_of::<usize>();
|
||||
|
||||
// If brand new:
|
||||
if new {
|
||||
// grow to initial size
|
||||
len = EVENT_MAP_CHUNK;
|
||||
event_map_file.set_len(EVENT_MAP_CHUNK as u64)?;
|
||||
}
|
||||
|
||||
// Memory map it
|
||||
let event_map = unsafe { MmapAppend::new(&event_map_file, new)? };
|
||||
|
||||
log::info!(
|
||||
"Event Store: new={:?} end={} len={}",
|
||||
new,
|
||||
event_map.get_end(),
|
||||
len
|
||||
);
|
||||
|
||||
Ok(EventStore {
|
||||
event_map_file,
|
||||
event_map_file_len: AtomicUsize::new(len),
|
||||
event_map,
|
||||
aligned,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get whether events are aligned in the map
|
||||
#[inline]
|
||||
pub fn is_aligned(&self) -> bool {
|
||||
self.aligned
|
||||
}
|
||||
|
||||
/// Get the number of bytes used in the event map
|
||||
#[inline]
|
||||
pub fn read_event_map_end(&self) -> usize {
|
||||
self.event_map.get_end()
|
||||
}
|
||||
|
||||
/// Get an event by its offset in the map
|
||||
pub fn get_event_by_offset(&self, offset: usize) -> Result<Event, Error> {
|
||||
if offset >= self.read_event_map_end() {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
let event = Event::delineate(&self.event_map[offset..])?;
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
// This stores an event
|
||||
// It does NOT validate the event.
|
||||
// It does NOT check first if the event is already stored, so it could store a duplicate
|
||||
// It does NOT record the event into any indexes
|
||||
// But it does grow the file if needed and returns the offset where it was stored
|
||||
pub fn store_event(&self, event: &Event) -> Result<usize, Error> {
|
||||
if self.aligned {
|
||||
let mut end = self.event_map.get_end();
|
||||
if end % 8 != 0 {
|
||||
let padding = 8 - (end % 8);
|
||||
end += padding;
|
||||
assert_eq!(end % 8, 0);
|
||||
self.event_map.append(padding, |_| Ok(padding))?;
|
||||
}
|
||||
}
|
||||
|
||||
let event_size = event.length();
|
||||
|
||||
loop {
|
||||
let result = self.event_map.append(event_size, |dst| event.macopy(dst));
|
||||
|
||||
match result {
|
||||
Ok(offset) => return Ok(offset),
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::Other {
|
||||
if e.to_string() == "Out of space" {
|
||||
// Determine the new size
|
||||
let new_file_len = {
|
||||
let file_len = self.event_map_file_len.load(Ordering::Relaxed);
|
||||
file_len + EVENT_MAP_CHUNK
|
||||
};
|
||||
|
||||
// Grow the file
|
||||
self.event_map_file.set_len(new_file_len as u64)?;
|
||||
|
||||
// Resize the memory map
|
||||
self.event_map.resize(new_file_len)?;
|
||||
|
||||
// Save this new length
|
||||
self.event_map_file_len
|
||||
.store(new_file_len, Ordering::Relaxed);
|
||||
|
||||
// Try again
|
||||
continue;
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> EventStoreIter<'_> {
|
||||
EventStoreIter {
|
||||
store: self,
|
||||
offset: mmap_append::HEADER_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventStoreIter<'a> {
|
||||
store: &'a EventStore,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for EventStoreIter<'a> {
|
||||
type Item = Result<Event<'a>, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.store.get_event_by_offset(self.offset) {
|
||||
Err(e) => {
|
||||
if matches!(e.inner, ChorusError::EndOfInput) {
|
||||
None
|
||||
} else {
|
||||
Some(Err(e))
|
||||
}
|
||||
}
|
||||
Ok(event) => {
|
||||
self.offset += event.length();
|
||||
if self.store.aligned && self.offset % 8 != 0 {
|
||||
self.offset += 8 - (self.offset % 8);
|
||||
assert_eq!(self.offset % 8, 0);
|
||||
}
|
||||
Some(Ok(event))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::store::EventStore;
|
||||
use crate::types::Event;
|
||||
|
||||
#[test]
|
||||
fn test_event_store() {
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let path = tempdir.path().join("mmap");
|
||||
let store = EventStore::new(&path, false).unwrap();
|
||||
|
||||
println!("Event map has {} used bytes", store.read_event_map_end());
|
||||
|
||||
let e1str = br#"{"id":"000000005ccb8402fe9af2ecc72ca1dbbf2dbeb9a0c6353b7f8198a65106f04a","pubkey":"7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805","created_at":1677930312,"kind":1,"tags":[["p","fe1d10131ca6103715d261f1615a8cd31f6b68a3d1a3272aab8e9e83f787126e"],["e","f76a6f60df029c388e98d4e97c67424cb71e34455302580d3aa48d0be96dfaef","wss://nos.lol","root"],["e","f529676213b123f55117ae9b93ca7dd6c85c872b7c260da7dbf60d71875879f6","wss://brb.io","reply"],["nonce","12912720851603613541","25"]],"content":"https://www.nostr.guru/p/fe1d10131ca6103715d261f1615a8cd31f6b68a3d1a3272aab8e9e83f787126e\nHere I can see only the today contact list.\n\nI tried to query relay.damus.io and nostr.wine but I can get only this last contact list, perhaps they have a time limit; you could ask some relay owner for a backup.\nIf can be useful this is the REQ filter to use:\n{\"kinds\": [3], \"authors\": [\"fe1d10131ca6103715d261f1615a8cd31f6b68a3d1a3272aab8e9e83f787126e\"], \"since\": 1672569529}","sig":"7c72d15cf9b4244cb8c49564d3735d0ac620dca69253956975def12054bc50217307f062f3e9ea879de8bae756c844a9315f03aabbb5f5fd94f9fc9beb76f457"}"#;
|
||||
let mut buffer1: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer1.resize(4096, 0);
|
||||
let (_insize, event1) = Event::from_json(&e1str[..], &mut buffer1).unwrap();
|
||||
let offset1 = store.store_event(&event1).unwrap();
|
||||
|
||||
let e2str = br#"{"id":"00000000a6fa8ee15b17fcc5bb49f09f85c15cddf5349986db09fddc0a123f7d","pubkey":"7cc328a08ddb2afdf9f9be77beff4c83489ff979721827d628a542f32a247c0e","created_at":1678835260,"kind":1,"tags":[["p","0000000033f569c7069cdec575ca000591a31831ebb68de20ed9fb783e3fc287"],["e","52338357568d06379ad2412a2a2033f23f224754d74148bbfc659b872c9477c4","wss://relay.damus.io/","root"],["nonce","9223372036941030112","32"]],"content":"oh shit, it got released?! no wayyy","sig":"ec9bcdcaa843a2a275857999556136d2b3cc47dac52c899d313dd70291b9661590d4c10b13680bbfa685d67d319798cdfa3ca58f61af5f8acffd0e25bca5ab95"}"#;
|
||||
let mut buffer2: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer2.resize(4096, 0);
|
||||
let (_insize, event2) = Event::from_json(&e2str[..], &mut buffer2).unwrap();
|
||||
let offset2 = store.store_event(&event2).unwrap();
|
||||
|
||||
let e3str = br#"{"id":"00000000ad0efde5b63e9b24b12a586dc98df372e1fd6f96ac6ad24ea2ed1350","pubkey":"c5fb6ecc876e0458e3eca9918e370cbcd376901c58460512fe537a46e58c38bb","created_at":1681739201,"kind":7,"tags":[["e","193bd20beb8fc13f4218ea106928c3be81ee3b2ad2b1bdbdd2c55efd859a195a","wss://eden.nostr.land/"],["p","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["client","gossip"],["nonce","2305843009213833122","22"]],"content":"+","sig":"f344111c221d2fea5f006865b98b0767b40ed1cc2907d8a325a8dea4b98414d008296ff4f4bd4666d52ec86ffd4739e807c6655ce43de98b473326e30957fcb2"}"#;
|
||||
let mut buffer3: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer3.resize(4096, 0);
|
||||
let (_insize, event3) = Event::from_json(&e3str[..], &mut buffer3).unwrap();
|
||||
let offset3 = store.store_event(&event3).unwrap();
|
||||
|
||||
println!("Event map has {} used bytes", store.read_event_map_end());
|
||||
|
||||
if let Ok(event) = store.get_event_by_offset(offset1) {
|
||||
assert_eq!(event, event1);
|
||||
} else {
|
||||
panic!("EVENT 1 IS WRONG");
|
||||
}
|
||||
|
||||
if let Ok(event) = store.get_event_by_offset(offset2) {
|
||||
assert_eq!(event, event2);
|
||||
} else {
|
||||
panic!("EVENT 2 IS WRONG");
|
||||
}
|
||||
|
||||
if let Ok(event) = store.get_event_by_offset(offset3) {
|
||||
assert_eq!(event, event3);
|
||||
} else {
|
||||
panic!("EVENT 3 IS WRONG");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,766 +0,0 @@
|
||||
use crate::error::Error;
|
||||
use crate::ip::{HashedIp, IpData};
|
||||
use crate::types::{Event, Id, Kind, Pubkey, Time};
|
||||
use heed::types::{OwnedType, UnalignedSlice, Unit, U8};
|
||||
use heed::{Database, Env, EnvFlags, EnvOpenOptions, RoIter, RoRange, RoTxn, RwTxn};
|
||||
use speedy::{Readable, Writable};
|
||||
use std::fs;
|
||||
use std::ops::Bound;
|
||||
|
||||
mod retired;
|
||||
|
||||
const FALSE: &[u8] = &[0];
|
||||
const TRUE: &[u8] = &[1];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Lmdb {
|
||||
env: Env,
|
||||
general: Database<UnalignedSlice<u8>, UnalignedSlice<u8>>,
|
||||
i_index: Database<UnalignedSlice<u8>, OwnedType<usize>>,
|
||||
ci_index: Database<UnalignedSlice<u8>, OwnedType<usize>>,
|
||||
tc_index: Database<UnalignedSlice<u8>, OwnedType<usize>>,
|
||||
ac_index: Database<UnalignedSlice<u8>, OwnedType<usize>>,
|
||||
akc_index: Database<UnalignedSlice<u8>, OwnedType<usize>>,
|
||||
atc_index: Database<UnalignedSlice<u8>, OwnedType<usize>>,
|
||||
ktc_index: Database<UnalignedSlice<u8>, OwnedType<usize>>,
|
||||
|
||||
// this is for events deleted by other events
|
||||
deleted_ids: Database<UnalignedSlice<u8>, Unit>,
|
||||
approved_events: Database<UnalignedSlice<u8>, U8>,
|
||||
approved_pubkeys: Database<UnalignedSlice<u8>, U8>,
|
||||
ip_data: Database<UnalignedSlice<u8>, UnalignedSlice<u8>>,
|
||||
}
|
||||
|
||||
impl Lmdb {
|
||||
pub fn new(directory: &str) -> Result<Lmdb, Error> {
|
||||
let mut builder = EnvOpenOptions::new();
|
||||
unsafe {
|
||||
builder.flags(EnvFlags::NO_TLS);
|
||||
}
|
||||
builder.max_dbs(32);
|
||||
builder.map_size(1048576 * 1024 * 24); // 24 GB
|
||||
|
||||
fs::create_dir_all(directory)?;
|
||||
|
||||
let env = match builder.open(directory) {
|
||||
Ok(env) => env,
|
||||
Err(e) => {
|
||||
log::error!("Unable to open LMDB at {}", directory);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Open/Create maps
|
||||
let mut txn = env.write_txn()?;
|
||||
let general = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, UnalignedSlice<u8>>()
|
||||
.create(&mut txn)?;
|
||||
let i_index = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, OwnedType<usize>>()
|
||||
.name("ids")
|
||||
.create(&mut txn)?;
|
||||
let ci_index = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, OwnedType<usize>>()
|
||||
.name("ci")
|
||||
.create(&mut txn)?;
|
||||
let tc_index = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, OwnedType<usize>>()
|
||||
.name("tci")
|
||||
.create(&mut txn)?;
|
||||
let ac_index = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, OwnedType<usize>>()
|
||||
.name("aci")
|
||||
.create(&mut txn)?;
|
||||
let akc_index = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, OwnedType<usize>>()
|
||||
.name("akci")
|
||||
.create(&mut txn)?;
|
||||
let atc_index = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, OwnedType<usize>>()
|
||||
.name("atci")
|
||||
.create(&mut txn)?;
|
||||
let ktc_index = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, OwnedType<usize>>()
|
||||
.name("ktci")
|
||||
.create(&mut txn)?;
|
||||
let deleted_ids = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, Unit>()
|
||||
.name("deleted-ids")
|
||||
.create(&mut txn)?;
|
||||
let approved_events = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, U8>()
|
||||
.name("approved-events")
|
||||
.create(&mut txn)?;
|
||||
let approved_pubkeys = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, U8>()
|
||||
.name("approved-pubkeys")
|
||||
.create(&mut txn)?;
|
||||
let ip_data = env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, UnalignedSlice<u8>>()
|
||||
.name("ip_data")
|
||||
.create(&mut txn)?;
|
||||
txn.commit()?;
|
||||
|
||||
let lmdb = Lmdb {
|
||||
env,
|
||||
general,
|
||||
i_index,
|
||||
ci_index,
|
||||
tc_index,
|
||||
ac_index,
|
||||
akc_index,
|
||||
atc_index,
|
||||
ktc_index,
|
||||
deleted_ids,
|
||||
approved_events,
|
||||
approved_pubkeys,
|
||||
ip_data,
|
||||
};
|
||||
|
||||
Ok(lmdb)
|
||||
}
|
||||
|
||||
/// Sync the data to disk. This happens periodically, but sometimes it's useful to force
|
||||
/// it.
|
||||
pub fn sync(&self) -> Result<(), Error> {
|
||||
self.env.force_sync()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a read transaction
|
||||
pub fn read_txn(&self) -> Result<RoTxn, Error> {
|
||||
Ok(self.env.read_txn()?)
|
||||
}
|
||||
|
||||
/// Get a write transaction
|
||||
pub fn write_txn(&self) -> Result<RwTxn, Error> {
|
||||
Ok(self.env.write_txn()?)
|
||||
}
|
||||
|
||||
pub fn log_stats(&self) -> Result<(), Error> {
|
||||
let txn = self.read_txn()?;
|
||||
|
||||
if !self.get_if_events_are_aligned()? {
|
||||
log::warn!("Events are not aligned on 8-byte offsets. You should run `chorus_compress` to rebuild your data (but it will keep working regardless)");
|
||||
}
|
||||
|
||||
if let Ok(count) = self.i_index.len(&txn) {
|
||||
log::info!("Index: id ({} entries, {} bytes)", count, count * (32 + 8));
|
||||
}
|
||||
if let Ok(count) = self.ci_index.len(&txn) {
|
||||
log::info!(
|
||||
"Index: created_at+id ({} entries, {} bytes)",
|
||||
count,
|
||||
count * (40 + 8)
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(count) = self.tc_index.len(&txn) {
|
||||
log::info!(
|
||||
"Index: tag+created_at+id ({} entries, {} bytes)",
|
||||
count,
|
||||
count * (223 + 8)
|
||||
);
|
||||
}
|
||||
if let Ok(count) = self.ac_index.len(&txn) {
|
||||
log::info!(
|
||||
"Index: author+created_at+id ({} entries, {} bytes)",
|
||||
count,
|
||||
count * (72 + 8)
|
||||
);
|
||||
}
|
||||
if let Ok(count) = self.akc_index.len(&txn) {
|
||||
log::info!(
|
||||
"Index: author+kind+created_at+id ({} entries, {} bytes)",
|
||||
count,
|
||||
count * (74 + 8)
|
||||
);
|
||||
}
|
||||
if let Ok(count) = self.atc_index.len(&txn) {
|
||||
log::info!(
|
||||
"Index: author+tags+created_at+id ({} entries, {} bytes)",
|
||||
count,
|
||||
count * (255 + 8)
|
||||
);
|
||||
}
|
||||
if let Ok(count) = self.ktc_index.len(&txn) {
|
||||
log::info!(
|
||||
"Index: kind+tags+created_at+id ({} entries, {} bytes)",
|
||||
count,
|
||||
count * (225 + 8)
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(count) = self.deleted_ids.len(&txn) {
|
||||
log::info!("{} deleted events", count);
|
||||
}
|
||||
if let Ok(count) = self.ip_data.len(&txn) {
|
||||
log::info!("{count} IP addresses reputationally tracked");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_migration_level(&self, txn: &RoTxn<'_>) -> Result<u32, Error> {
|
||||
let zero_bytes = 0_u32.to_be_bytes();
|
||||
let migration_level_bytes = self
|
||||
.general
|
||||
.get(txn, b"migration_level")?
|
||||
.unwrap_or(zero_bytes.as_slice());
|
||||
Ok(u32::from_be_bytes(
|
||||
migration_level_bytes[..4].try_into().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_migration_level(&self, txn: &mut RwTxn<'_>, level: u32) -> Result<(), Error> {
|
||||
self.general
|
||||
.put(txn, b"migration_level", level.to_be_bytes().as_slice())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_if_events_are_aligned(&self) -> Result<bool, Error> {
|
||||
let txn = self.read_txn()?;
|
||||
match self.general.get(&txn, b"events_are_aligned")? {
|
||||
None => Ok(false),
|
||||
Some(bytes) => match bytes[0] {
|
||||
0 => Ok(false),
|
||||
_ => Ok(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_if_events_are_aligned(
|
||||
&self,
|
||||
txn: &mut RwTxn<'_>,
|
||||
events_are_aligned: bool,
|
||||
) -> Result<(), Error> {
|
||||
let slice = match events_are_aligned {
|
||||
false => FALSE,
|
||||
true => TRUE,
|
||||
};
|
||||
self.general.put(txn, b"events_are_aligned", slice)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Index the event
|
||||
pub fn index(&self, txn: &mut RwTxn<'_>, event: &Event, offset: usize) -> Result<(), Error> {
|
||||
// Index by id
|
||||
self.i_index.put(txn, event.id().0.as_slice(), &offset)?;
|
||||
|
||||
// Index by created_at and id
|
||||
self.ci_index.put(
|
||||
txn,
|
||||
&Self::key_ci_index(event.created_at(), event.id()),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
// Index by author and kind (with created_at and id)
|
||||
self.akc_index.put(
|
||||
txn,
|
||||
&Self::key_akc_index(event.pubkey(), event.kind(), event.created_at(), event.id()),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
self.ac_index.put(
|
||||
txn,
|
||||
&Self::key_ac_index(event.pubkey(), event.created_at(), event.id()),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
for mut tsi in event.tags()?.iter() {
|
||||
if let Some(tagname) = tsi.next() {
|
||||
// FIXME make sure it is a letter too
|
||||
if tagname.len() == 1 {
|
||||
if let Some(tagvalue) = tsi.next() {
|
||||
// Index by tag (with created_at and id)
|
||||
self.tc_index.put(
|
||||
txn,
|
||||
&Self::key_tc_index(
|
||||
tagname[0],
|
||||
tagvalue,
|
||||
event.created_at(),
|
||||
event.id(),
|
||||
),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
// Index by author and tag (with created_at and id)
|
||||
self.atc_index.put(
|
||||
txn,
|
||||
&Self::key_atc_index(
|
||||
event.pubkey(),
|
||||
tagname[0],
|
||||
tagvalue,
|
||||
event.created_at(),
|
||||
event.id(),
|
||||
),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
// Index by kind and tag (with created_at and id)
|
||||
self.ktc_index.put(
|
||||
txn,
|
||||
&Self::key_ktc_index(
|
||||
event.kind(),
|
||||
tagname[0],
|
||||
tagvalue,
|
||||
event.created_at(),
|
||||
event.id(),
|
||||
),
|
||||
&offset,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Remove the event from all indexes (except the 'id' index)
|
||||
pub fn deindex(&self, txn: &mut RwTxn<'_>, event: &Event) -> Result<(), Error> {
|
||||
for mut tsi in event.tags()?.iter() {
|
||||
if let Some(tagname) = tsi.next() {
|
||||
// FIXME make sure it is a letter too
|
||||
if tagname.len() == 1 {
|
||||
if let Some(tagvalue) = tsi.next() {
|
||||
// Index by author and tag (with created_at and id)
|
||||
self.atc_index.delete(
|
||||
txn,
|
||||
&Self::key_atc_index(
|
||||
event.pubkey(),
|
||||
tagname[0],
|
||||
tagvalue,
|
||||
event.created_at(),
|
||||
event.id(),
|
||||
),
|
||||
)?;
|
||||
|
||||
// Index by kind and tag (with created_at and id)
|
||||
self.ktc_index.delete(
|
||||
txn,
|
||||
&Self::key_ktc_index(
|
||||
event.kind(),
|
||||
tagname[0],
|
||||
tagvalue,
|
||||
event.created_at(),
|
||||
event.id(),
|
||||
),
|
||||
)?;
|
||||
|
||||
// Index by tag (with created_at and id)
|
||||
self.tc_index.delete(
|
||||
txn,
|
||||
&Self::key_tc_index(
|
||||
tagname[0],
|
||||
tagvalue,
|
||||
event.created_at(),
|
||||
event.id(),
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.ac_index.delete(
|
||||
txn,
|
||||
&Self::key_ac_index(event.pubkey(), event.created_at(), event.id()),
|
||||
)?;
|
||||
|
||||
self.ci_index
|
||||
.delete(txn, &Self::key_ci_index(event.created_at(), event.id()))?;
|
||||
|
||||
self.akc_index.delete(
|
||||
txn,
|
||||
&Self::key_akc_index(event.pubkey(), event.kind(), event.created_at(), event.id()),
|
||||
)?;
|
||||
|
||||
// We leave it in the id map. If someone wants to load the replaced event by id
|
||||
// they can still do it.
|
||||
// self.i_index.delete(&mut txn, event.id().0.as_slice())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deindex_id(&self, txn: &mut RwTxn<'_>, id: Id) -> Result<(), Error> {
|
||||
self.i_index.delete(txn, id.0.as_slice())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_offset_by_id(&self, txn: &RoTxn<'_>, id: Id) -> Result<Option<usize>, Error> {
|
||||
Ok(self.i_index.get(txn, id.0.as_slice())?)
|
||||
}
|
||||
|
||||
pub fn is_deleted(&self, txn: &RoTxn<'_>, id: Id) -> Result<bool, Error> {
|
||||
Ok(self.deleted_ids.get(txn, id.as_slice())?.is_some())
|
||||
}
|
||||
|
||||
pub fn mark_deleted(&self, txn: &mut RwTxn<'_>, id: Id) -> Result<(), Error> {
|
||||
self.deleted_ids.put(txn, id.as_slice(), &())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dump_deleted(&self) -> Result<Vec<Id>, Error> {
|
||||
let mut output: Vec<Id> = Vec::new();
|
||||
let txn = self.read_txn()?;
|
||||
for i in self.deleted_ids.iter(&txn)? {
|
||||
let (key, _val) = i?;
|
||||
let id = Id(key[0..32].try_into().unwrap());
|
||||
output.push(id);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn get_ip_data(&self, ip: HashedIp) -> Result<IpData, Error> {
|
||||
let key = &ip.0;
|
||||
let txn = self.read_txn()?;
|
||||
let bytes = match self.ip_data.get(&txn, key)? {
|
||||
Some(b) => b,
|
||||
None => return Ok(Default::default()),
|
||||
};
|
||||
Ok(IpData::read_from_buffer(bytes)?)
|
||||
}
|
||||
|
||||
pub fn update_ip_data(&self, ip: HashedIp, data: &IpData) -> Result<(), Error> {
|
||||
let key = &ip.0;
|
||||
let mut txn = self.write_txn()?;
|
||||
let bytes = data.write_to_vec()?;
|
||||
self.ip_data.put(&mut txn, key, &bytes)?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dump_ip_data(&self) -> Result<Vec<(HashedIp, IpData)>, Error> {
|
||||
let mut output: Vec<(HashedIp, IpData)> = Vec::new();
|
||||
let txn = self.read_txn()?;
|
||||
for i in self.ip_data.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let hashedip = HashedIp::from_bytes(key);
|
||||
let data = IpData::read_from_buffer(val)?;
|
||||
output.push((hashedip, data));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn mark_event_approval(&self, id: Id, approval: bool) -> Result<(), Error> {
|
||||
let mut txn = self.write_txn()?;
|
||||
self.approved_events
|
||||
.put(&mut txn, id.0.as_slice(), &(approval as u8))?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_event_approval(&self, id: Id) -> Result<(), Error> {
|
||||
let mut txn = self.write_txn()?;
|
||||
self.approved_events.delete(&mut txn, id.0.as_slice())?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_event_approval(&self, id: Id) -> Result<Option<bool>, Error> {
|
||||
let txn = self.read_txn()?;
|
||||
Ok(self
|
||||
.approved_events
|
||||
.get(&txn, id.0.as_slice())?
|
||||
.map(|u| u != 0))
|
||||
}
|
||||
|
||||
pub fn dump_event_approvals(&self) -> Result<Vec<(Id, bool)>, Error> {
|
||||
let mut output: Vec<(Id, bool)> = Vec::new();
|
||||
let txn = self.read_txn()?;
|
||||
for i in self.approved_events.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let id = Id(key.try_into().unwrap());
|
||||
let approval: bool = val != 0;
|
||||
output.push((id, approval));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn mark_pubkey_approval(&self, pubkey: Pubkey, approval: bool) -> Result<(), Error> {
|
||||
let mut txn = self.write_txn()?;
|
||||
self.approved_pubkeys
|
||||
.put(&mut txn, pubkey.0.as_slice(), &(approval as u8))?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_pubkey_approval(&self, pubkey: Pubkey) -> Result<(), Error> {
|
||||
let mut txn = self.write_txn()?;
|
||||
self.approved_pubkeys
|
||||
.delete(&mut txn, pubkey.0.as_slice())?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_pubkey_approval(&self, pubkey: Pubkey) -> Result<Option<bool>, Error> {
|
||||
let txn = self.read_txn()?;
|
||||
Ok(self
|
||||
.approved_pubkeys
|
||||
.get(&txn, pubkey.0.as_slice())?
|
||||
.map(|u| u != 0))
|
||||
}
|
||||
|
||||
pub fn dump_pubkey_approvals(&self) -> Result<Vec<(Pubkey, bool)>, Error> {
|
||||
let mut output: Vec<(Pubkey, bool)> = Vec::new();
|
||||
let txn = self.read_txn()?;
|
||||
for i in self.approved_pubkeys.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let pubkey = Pubkey(key.try_into().unwrap());
|
||||
let approval: bool = val != 0;
|
||||
output.push((pubkey, approval));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn i_iter<'a>(
|
||||
&'a self,
|
||||
txn: &'a RoTxn,
|
||||
) -> Result<RoIter<'_, UnalignedSlice<u8>, OwnedType<usize>>, Error> {
|
||||
Ok(self.i_index.iter(txn)?)
|
||||
}
|
||||
|
||||
pub fn ci_iter<'a>(
|
||||
&'a self,
|
||||
since: Time,
|
||||
until: Time,
|
||||
txn: &'a RoTxn,
|
||||
) -> Result<RoRange<'_, UnalignedSlice<u8>, OwnedType<usize>>, Error> {
|
||||
let start_prefix = Self::key_ci_index(until, Id([0; 32]));
|
||||
let end_prefix = Self::key_ci_index(since, Id([255; 32]));
|
||||
let range = (
|
||||
Bound::Included(&*start_prefix),
|
||||
Bound::Excluded(&*end_prefix),
|
||||
);
|
||||
Ok(self.ci_index.range(txn, &range)?)
|
||||
}
|
||||
|
||||
pub fn tc_iter<'a>(
|
||||
&'a self,
|
||||
tagbyte: u8,
|
||||
tagvalue: &[u8],
|
||||
since: Time,
|
||||
until: Time,
|
||||
txn: &'a RoTxn,
|
||||
) -> Result<RoRange<'_, UnalignedSlice<u8>, OwnedType<usize>>, Error> {
|
||||
let start_prefix = Self::key_tc_index(
|
||||
tagbyte,
|
||||
tagvalue,
|
||||
until, // scan goes backwards in time
|
||||
Id([0; 32]),
|
||||
);
|
||||
let end_prefix = Self::key_tc_index(tagbyte, tagvalue, since, Id([255; 32]));
|
||||
let range = (
|
||||
Bound::Included(&*start_prefix),
|
||||
Bound::Excluded(&*end_prefix),
|
||||
);
|
||||
Ok(self.tc_index.range(txn, &range)?)
|
||||
}
|
||||
|
||||
pub fn ac_iter<'a>(
|
||||
&'a self,
|
||||
author: Pubkey,
|
||||
since: Time,
|
||||
until: Time,
|
||||
txn: &'a RoTxn,
|
||||
) -> Result<RoRange<'_, UnalignedSlice<u8>, OwnedType<usize>>, Error> {
|
||||
let start_prefix = Self::key_ac_index(author, until, Id([0; 32]));
|
||||
let end_prefix = Self::key_ac_index(author, since, Id([255; 32]));
|
||||
let range = (
|
||||
Bound::Included(&*start_prefix),
|
||||
Bound::Excluded(&*end_prefix),
|
||||
);
|
||||
Ok(self.ac_index.range(txn, &range)?)
|
||||
}
|
||||
|
||||
pub fn akc_iter<'a>(
|
||||
&'a self,
|
||||
author: Pubkey,
|
||||
kind: Kind,
|
||||
since: Time,
|
||||
until: Time,
|
||||
txn: &'a RoTxn,
|
||||
) -> Result<RoRange<'_, UnalignedSlice<u8>, OwnedType<usize>>, Error> {
|
||||
let start_prefix = Self::key_akc_index(author, kind, until, Id([0; 32]));
|
||||
let end_prefix = Self::key_akc_index(author, kind, since, Id([255; 32]));
|
||||
let range = (
|
||||
Bound::Included(&*start_prefix),
|
||||
Bound::Excluded(&*end_prefix),
|
||||
);
|
||||
Ok(self.akc_index.range(txn, &range)?)
|
||||
}
|
||||
|
||||
pub fn atc_iter<'a>(
|
||||
&'a self,
|
||||
author: Pubkey,
|
||||
tagbyte: u8,
|
||||
tagvalue: &[u8],
|
||||
since: Time,
|
||||
until: Time,
|
||||
txn: &'a RoTxn,
|
||||
) -> Result<RoRange<'_, UnalignedSlice<u8>, OwnedType<usize>>, Error> {
|
||||
let start_prefix = Self::key_atc_index(
|
||||
author,
|
||||
tagbyte,
|
||||
tagvalue,
|
||||
until, // scan goes backwards in time
|
||||
Id([0; 32]),
|
||||
);
|
||||
let end_prefix = Self::key_atc_index(author, tagbyte, tagvalue, since, Id([255; 32]));
|
||||
let range = (
|
||||
Bound::Included(&*start_prefix),
|
||||
Bound::Excluded(&*end_prefix),
|
||||
);
|
||||
Ok(self.atc_index.range(txn, &range)?)
|
||||
}
|
||||
|
||||
pub fn ktc_iter<'a>(
|
||||
&'a self,
|
||||
kind: Kind,
|
||||
tagbyte: u8,
|
||||
tagvalue: &[u8],
|
||||
since: Time,
|
||||
until: Time,
|
||||
txn: &'a RoTxn,
|
||||
) -> Result<RoRange<'_, UnalignedSlice<u8>, OwnedType<usize>>, Error> {
|
||||
let start_prefix = Self::key_ktc_index(
|
||||
kind,
|
||||
tagbyte,
|
||||
tagvalue,
|
||||
until, // scan goes backwards in time
|
||||
Id([0; 32]),
|
||||
);
|
||||
let end_prefix = Self::key_ktc_index(kind, tagbyte, tagvalue, since, Id([255; 32]));
|
||||
let range = (
|
||||
Bound::Included(&*start_prefix),
|
||||
Bound::Excluded(&*end_prefix),
|
||||
);
|
||||
Ok(self.ktc_index.range(txn, &range)?)
|
||||
}
|
||||
|
||||
fn key_ci_index(created_at: Time, id: Id) -> Vec<u8> {
|
||||
let mut key: Vec<u8> =
|
||||
Vec::with_capacity(std::mem::size_of::<Time>() + std::mem::size_of::<Id>());
|
||||
key.extend((u64::MAX - created_at.0).to_be_bytes().as_slice());
|
||||
key.extend(id.as_slice());
|
||||
key
|
||||
}
|
||||
|
||||
// For looking up event by Tag
|
||||
// tagletter(1) + fixlentag(182) + reversecreatedat(8) + id(32)
|
||||
fn key_tc_index(letter: u8, tag_value: &[u8], created_at: Time, id: Id) -> Vec<u8> {
|
||||
const PADLEN: usize = 182;
|
||||
let mut key: Vec<u8> =
|
||||
Vec::with_capacity(PADLEN + std::mem::size_of::<Time>() + std::mem::size_of::<Id>());
|
||||
key.push(letter);
|
||||
if tag_value.len() <= PADLEN {
|
||||
key.extend(tag_value);
|
||||
key.extend(core::iter::repeat(0).take(PADLEN - tag_value.len()));
|
||||
} else {
|
||||
key.extend(&tag_value[..PADLEN]);
|
||||
}
|
||||
key.extend((u64::MAX - created_at.0).to_be_bytes().as_slice());
|
||||
key.extend(id.as_slice());
|
||||
key
|
||||
}
|
||||
|
||||
// For looking up event by Author
|
||||
// author(32) + reversecreatedat(8) + id(32)
|
||||
fn key_ac_index(author: Pubkey, created_at: Time, id: Id) -> Vec<u8> {
|
||||
let mut key: Vec<u8> = Vec::with_capacity(
|
||||
std::mem::size_of::<Pubkey>() + std::mem::size_of::<Time>() + std::mem::size_of::<Id>(),
|
||||
);
|
||||
key.extend(author.as_slice());
|
||||
key.extend((u64::MAX - created_at.0).to_be_bytes().as_slice());
|
||||
key.extend(id.as_slice());
|
||||
key
|
||||
}
|
||||
|
||||
// For looking up event by Author and Kind
|
||||
// author(32) + kind(2) + reversecreatedat(8) + id(32)
|
||||
fn key_akc_index(author: Pubkey, kind: Kind, created_at: Time, id: Id) -> Vec<u8> {
|
||||
let mut key: Vec<u8> = Vec::with_capacity(
|
||||
std::mem::size_of::<Pubkey>()
|
||||
+ std::mem::size_of::<Kind>()
|
||||
+ std::mem::size_of::<Time>()
|
||||
+ std::mem::size_of::<Id>(),
|
||||
);
|
||||
key.extend(author.as_slice());
|
||||
key.extend(kind.0.to_be_bytes());
|
||||
key.extend((u64::MAX - created_at.0).to_be_bytes().as_slice());
|
||||
key.extend(id.as_slice());
|
||||
key
|
||||
}
|
||||
|
||||
// For looking up event by Author and Tag
|
||||
// author(32) + tagletter(1) + fixlentag(182) + reversecreatedat(8) + id(32)
|
||||
fn key_atc_index(
|
||||
author: Pubkey,
|
||||
letter: u8,
|
||||
tag_value: &[u8],
|
||||
created_at: Time,
|
||||
id: Id,
|
||||
) -> Vec<u8> {
|
||||
const PADLEN: usize = 182;
|
||||
let mut key: Vec<u8> = Vec::with_capacity(
|
||||
std::mem::size_of::<Pubkey>()
|
||||
+ PADLEN
|
||||
+ std::mem::size_of::<Time>()
|
||||
+ std::mem::size_of::<Id>(),
|
||||
);
|
||||
key.extend(author.as_slice());
|
||||
key.push(letter);
|
||||
if tag_value.len() <= PADLEN {
|
||||
key.extend(tag_value);
|
||||
key.extend(core::iter::repeat(0).take(PADLEN - tag_value.len()));
|
||||
} else {
|
||||
key.extend(&tag_value[..PADLEN]);
|
||||
}
|
||||
key.extend((u64::MAX - created_at.0).to_be_bytes().as_slice());
|
||||
key.extend(id.as_slice());
|
||||
key
|
||||
}
|
||||
|
||||
// For looking up event by Kind and Tag
|
||||
// kind(2) + tagletter(1) + fixlentag(182) + reversecreatedat(8) + id(32)
|
||||
fn key_ktc_index(
|
||||
kind: Kind,
|
||||
letter: u8,
|
||||
tag_value: &[u8],
|
||||
created_at: Time,
|
||||
id: Id,
|
||||
) -> Vec<u8> {
|
||||
const PADLEN: usize = 182;
|
||||
let mut key: Vec<u8> = Vec::with_capacity(
|
||||
std::mem::size_of::<Kind>()
|
||||
+ PADLEN
|
||||
+ std::mem::size_of::<Time>()
|
||||
+ std::mem::size_of::<Id>(),
|
||||
);
|
||||
key.extend(kind.0.to_be_bytes());
|
||||
key.push(letter);
|
||||
if tag_value.len() <= PADLEN {
|
||||
key.extend(tag_value);
|
||||
key.extend(core::iter::repeat(0).take(PADLEN - tag_value.len()));
|
||||
} else {
|
||||
key.extend(&tag_value[..PADLEN]);
|
||||
}
|
||||
key.extend((u64::MAX - created_at.0).to_be_bytes().as_slice());
|
||||
key.extend(id.as_slice());
|
||||
key
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
use super::Lmdb;
|
||||
use crate::error::Error;
|
||||
use crate::types::Event;
|
||||
use heed::byteorder::BigEndian;
|
||||
use heed::types::{UnalignedSlice, Unit, U64};
|
||||
use heed::{Database, RwTxn};
|
||||
|
||||
impl Lmdb {
|
||||
pub fn deleted_offsets(
|
||||
&self,
|
||||
txn: &mut RwTxn,
|
||||
) -> Result<Database<U64<BigEndian>, Unit>, Error> {
|
||||
Ok(self
|
||||
.env
|
||||
.database_options()
|
||||
.types::<U64<BigEndian>, Unit>()
|
||||
.name("deleted_offsets")
|
||||
.create(txn)?)
|
||||
}
|
||||
|
||||
pub fn deleted_events(
|
||||
&self,
|
||||
txn: &mut RwTxn,
|
||||
) -> Result<Database<UnalignedSlice<u8>, Unit>, Error> {
|
||||
Ok(self
|
||||
.env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, Unit>()
|
||||
.name("deleted-events")
|
||||
.create(txn)?)
|
||||
}
|
||||
|
||||
// used in migrate_to_1
|
||||
pub fn index_ci_only(
|
||||
&self,
|
||||
txn: &mut RwTxn<'_>,
|
||||
event: &Event,
|
||||
offset: usize,
|
||||
) -> Result<(), Error> {
|
||||
self.ci_index.put(
|
||||
txn,
|
||||
&Self::key_ci_index(event.created_at(), event.id()),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// used in migrate_to_2
|
||||
pub fn index_ac_only(
|
||||
&self,
|
||||
txn: &mut RwTxn<'_>,
|
||||
event: &Event,
|
||||
offset: usize,
|
||||
) -> Result<(), Error> {
|
||||
self.ac_index.put(
|
||||
txn,
|
||||
&Self::key_ac_index(event.pubkey(), event.created_at(), event.id()),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// used in migrate_to_2
|
||||
pub fn index_tc_only(
|
||||
&self,
|
||||
txn: &mut RwTxn<'_>,
|
||||
tagbyte: u8,
|
||||
tagvalue: &[u8],
|
||||
event: &Event,
|
||||
offset: usize,
|
||||
) -> Result<(), Error> {
|
||||
self.tc_index.put(
|
||||
txn,
|
||||
&Self::key_tc_index(tagbyte, tagvalue, event.created_at(), event.id()),
|
||||
&offset,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// used in migrate_to_3
|
||||
pub fn clear_ip_data(&self, txn: &mut RwTxn<'_>) -> Result<(), Error> {
|
||||
self.ip_data.clear(txn)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
use super::Store;
|
||||
use crate::error::Error;
|
||||
use crate::types::Id;
|
||||
use heed::RwTxn;
|
||||
|
||||
pub const CURRENT_MIGRATION_LEVEL: u32 = 5;
|
||||
|
||||
impl Store {
|
||||
pub fn migrate(&self) -> Result<(), Error> {
|
||||
let mut txn = self.lmdb.write_txn()?;
|
||||
|
||||
let mut migration_level = self.lmdb.get_migration_level(&txn)?;
|
||||
|
||||
log::info!("Storage migration level = {}", migration_level);
|
||||
|
||||
while migration_level < CURRENT_MIGRATION_LEVEL {
|
||||
self.migrate_to(&mut txn, migration_level + 1)?;
|
||||
migration_level += 1;
|
||||
|
||||
self.lmdb.set_migration_level(&mut txn, migration_level)?
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_to(&self, txn: &mut RwTxn<'_>, level: u32) -> Result<(), Error> {
|
||||
log::info!("Migrating database to {}", level);
|
||||
match level {
|
||||
1 => self.migrate_to_1(txn)?,
|
||||
2 => self.migrate_to_2(txn)?,
|
||||
3 => self.migrate_to_3(txn)?,
|
||||
4 => self.migrate_to_4(txn)?,
|
||||
5 => self.migrate_to_5(txn)?,
|
||||
_ => panic!("Unknown migration level {level}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Populate ci_index
|
||||
fn migrate_to_1(&self, txn: &mut RwTxn<'_>) -> Result<(), Error> {
|
||||
let loop_txn = self.lmdb.read_txn()?;
|
||||
let iter = self.lmdb.i_iter(&loop_txn)?;
|
||||
for result in iter {
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
self.lmdb.index_ci_only(txn, &event, offset)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Populate tc_index and ac_index
|
||||
fn migrate_to_2(&self, txn: &mut RwTxn<'_>) -> Result<(), Error> {
|
||||
let loop_txn = self.lmdb.read_txn()?;
|
||||
let iter = self.lmdb.i_iter(&loop_txn)?;
|
||||
for result in iter {
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
// Add to ac_index
|
||||
self.lmdb.index_ac_only(txn, &event, offset)?;
|
||||
|
||||
// Add to tc_index
|
||||
for mut tsi in event.tags()?.iter() {
|
||||
if let Some(tagname) = tsi.next() {
|
||||
if tagname.len() == 1 {
|
||||
if let Some(tagvalue) = tsi.next() {
|
||||
self.lmdb
|
||||
.index_tc_only(txn, tagname[0], tagvalue, &event, offset)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Clear IP data (we are hashing now)
|
||||
fn migrate_to_3(&self, txn: &mut RwTxn<'_>) -> Result<(), Error> {
|
||||
self.lmdb.clear_ip_data(txn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Clear deleted_offsets (now retired)
|
||||
fn migrate_to_4(&self, txn: &mut RwTxn<'_>) -> Result<(), Error> {
|
||||
let deleted_offsets = self.lmdb.deleted_offsets(txn)?;
|
||||
deleted_offsets.clear(txn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Move data from deleted_events to deleted_ids
|
||||
fn migrate_to_5(&self, txn: &mut RwTxn<'_>) -> Result<(), Error> {
|
||||
let deleted_events = self.lmdb.deleted_events(txn)?;
|
||||
|
||||
let mut ids: Vec<Id> = Vec::new();
|
||||
|
||||
for i in deleted_events.iter(txn)? {
|
||||
let (key, _val) = i?;
|
||||
let id = Id(key[0..32].try_into().unwrap());
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
for id in ids.drain(..) {
|
||||
self.lmdb.mark_deleted(txn, id)?;
|
||||
}
|
||||
|
||||
deleted_events.clear(txn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,666 +0,0 @@
|
||||
pub mod event_store;
|
||||
pub use event_store::EventStore;
|
||||
|
||||
mod lmdb;
|
||||
use lmdb::Lmdb;
|
||||
|
||||
mod migrations;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::ip::{HashedIp, IpData};
|
||||
use crate::types::{Event, Filter, Id, Kind, Pubkey, Time};
|
||||
use heed::RwTxn;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Store {
|
||||
lmdb: Lmdb,
|
||||
events: EventStore,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Setup persistent storage
|
||||
pub fn new(config: &Config) -> Result<Store, Error> {
|
||||
let dir = format!("{}/lmdb", &config.data_directory);
|
||||
|
||||
// Create the lmdb subdir if it doesn't exist, ignoring errors
|
||||
let _ = std::fs::create_dir(&dir);
|
||||
|
||||
let lmdb = Lmdb::new(&dir)?;
|
||||
|
||||
let events_are_aligned = lmdb.get_if_events_are_aligned()?;
|
||||
|
||||
let events = {
|
||||
let event_map_file = format!("{}/event.map", &config.data_directory);
|
||||
EventStore::new(event_map_file, events_are_aligned)?
|
||||
};
|
||||
|
||||
let store = Store { lmdb, events };
|
||||
|
||||
// This is in migrations.rs
|
||||
store.migrate()?;
|
||||
|
||||
store.lmdb.log_stats()?;
|
||||
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
pub fn rebuild(config: &Config) -> Result<(), Error> {
|
||||
let dir = format!("{}/lmdb", &config.data_directory);
|
||||
let dir_bak = format!("{}/lmdb.bak", &config.data_directory);
|
||||
fs::rename(&dir, &dir_bak)?;
|
||||
let old_lmdb = Lmdb::new(&dir_bak)?;
|
||||
let new_lmdb = Lmdb::new(&dir)?;
|
||||
|
||||
let old_align = old_lmdb.get_if_events_are_aligned()?;
|
||||
|
||||
let event_map_file = format!("{}/event.map", &config.data_directory);
|
||||
let event_map_file_bak = format!("{}/event.map.bak", &config.data_directory);
|
||||
fs::rename(&event_map_file, &event_map_file_bak)?;
|
||||
|
||||
let old_event_store = EventStore::new(event_map_file_bak, old_align)?;
|
||||
let new_event_store = EventStore::new(event_map_file, true)?;
|
||||
|
||||
let old_store = Store {
|
||||
lmdb: old_lmdb,
|
||||
events: old_event_store,
|
||||
};
|
||||
|
||||
old_store.migrate()?;
|
||||
|
||||
let new_store = Store {
|
||||
lmdb: new_lmdb,
|
||||
events: new_event_store,
|
||||
};
|
||||
|
||||
new_store.migrate()?;
|
||||
|
||||
log::info!("Copying data...");
|
||||
|
||||
let old_txn = old_store.lmdb.read_txn()?;
|
||||
let mut new_txn = new_store.lmdb.write_txn()?;
|
||||
|
||||
new_store
|
||||
.lmdb
|
||||
.set_if_events_are_aligned(&mut new_txn, true)?;
|
||||
|
||||
// Iterate through all IDs and copy and index all of those events
|
||||
for i in old_store.lmdb.i_iter(&old_txn)? {
|
||||
let (_key, val) = i?;
|
||||
//let id = Id(key[0..32].try_into().unwrap());
|
||||
let old_offset: usize = val;
|
||||
let event = old_store.events.get_event_by_offset(old_offset)?;
|
||||
let new_offset = new_store.events.store_event(&event)?;
|
||||
new_store.lmdb.index(&mut new_txn, &event, new_offset)?;
|
||||
}
|
||||
|
||||
// Copy deleted IDs
|
||||
let mut deleted = old_store.lmdb.dump_deleted()?;
|
||||
for id in deleted.drain(..) {
|
||||
new_store.lmdb.mark_deleted(&mut new_txn, id)?;
|
||||
}
|
||||
|
||||
new_txn.commit()?;
|
||||
|
||||
// Copy approved events
|
||||
let mut approvals = old_store.dump_event_approvals()?;
|
||||
for (id, approval) in approvals.drain(..) {
|
||||
new_store.mark_event_approval(id, approval)?;
|
||||
}
|
||||
|
||||
// Copy approved pubkeys
|
||||
let mut approvals = old_store.dump_pubkey_approvals()?;
|
||||
for (pubkey, approval) in approvals.drain(..) {
|
||||
new_store.mark_pubkey_approval(pubkey, approval)?;
|
||||
}
|
||||
|
||||
// Copy ip data
|
||||
let mut ipdata = old_store.lmdb.dump_ip_data()?;
|
||||
for (hashedip, ipdata) in ipdata.drain(..) {
|
||||
new_store.update_ip_data(hashedip, &ipdata)?;
|
||||
}
|
||||
|
||||
new_store.lmdb.sync()?;
|
||||
|
||||
log::info!("done.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync the data to disk. This happens periodically, but sometimes it's useful to force
|
||||
/// it.
|
||||
pub fn sync(&self) -> Result<(), Error> {
|
||||
self.lmdb.sync()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store an event.
|
||||
///
|
||||
/// Returns the offset where the event is stored at, which can be used to fetch
|
||||
/// the event via get_event_by_offset().
|
||||
///
|
||||
/// If the event already exists, you will get a ChorusError::Duplicate
|
||||
///
|
||||
/// If the event is ephemeral, it will be stored and you will get an offset, but
|
||||
/// it will not be indexed.
|
||||
pub fn store_event(&self, event: &Event) -> Result<usize, Error> {
|
||||
// TBD: should we validate the event?
|
||||
|
||||
let mut txn = self.lmdb.write_txn()?;
|
||||
let offset;
|
||||
|
||||
// Only if it doesn't already exist
|
||||
if self.lmdb.get_offset_by_id(&txn, event.id())?.is_none() {
|
||||
// Reject event if it was deleted
|
||||
{
|
||||
if self.lmdb.is_deleted(&txn, event.id())? {
|
||||
return Err(ChorusError::Deleted.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Store the event
|
||||
offset = self.events.store_event(event)?;
|
||||
|
||||
// Index the event
|
||||
if !event.kind().is_ephemeral() {
|
||||
self.lmdb.index(&mut txn, event, offset)?;
|
||||
}
|
||||
|
||||
// If replaceable or parameterized replaceable,
|
||||
// find and delete all but the first one in the group
|
||||
if event.kind().is_replaceable() || event.kind().is_parameterized_replaceable() {
|
||||
self.delete_replaced(&mut txn, event)?;
|
||||
}
|
||||
|
||||
// Handle deletion events
|
||||
if event.kind() == Kind(5) {
|
||||
self.handle_deletion_event(&mut txn, event)?;
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
} else {
|
||||
return Err(ChorusError::Duplicate.into());
|
||||
}
|
||||
|
||||
Ok(offset)
|
||||
}
|
||||
|
||||
fn handle_deletion_event(&self, txn: &mut RwTxn<'_>, event: &Event) -> Result<(), Error> {
|
||||
for mut tag in event.tags()?.iter() {
|
||||
if let Some(tagname) = tag.next() {
|
||||
if tagname == b"e" {
|
||||
if let Some(id_hex) = tag.next() {
|
||||
if let Ok(id) = Id::read_hex(id_hex) {
|
||||
// Add deletion pair to the event_deleted table
|
||||
self.lmdb.mark_deleted(txn, id)?;
|
||||
|
||||
// Delete pair
|
||||
if let Some(target) = self.get_event_by_id(id)? {
|
||||
if target.pubkey() == event.pubkey() {
|
||||
self.delete_by_id(txn, id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get an event by its offset.
|
||||
pub fn get_event_by_offset(&self, offset: usize) -> Result<Event, Error> {
|
||||
self.events.get_event_by_offset(offset)
|
||||
}
|
||||
|
||||
/// Get an event by Id
|
||||
pub fn get_event_by_id(&self, id: Id) -> Result<Option<Event>, Error> {
|
||||
let txn = self.lmdb.read_txn()?;
|
||||
if let Some(offset) = self.lmdb.get_offset_by_id(&txn, id)? {
|
||||
Some(self.events.get_event_by_offset(offset)).transpose()
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all events that match the filter
|
||||
pub fn find_events<F>(
|
||||
&self,
|
||||
filter: Filter,
|
||||
screen: F,
|
||||
config: &Config,
|
||||
) -> Result<Vec<Event>, Error>
|
||||
where
|
||||
F: Fn(&Event) -> bool,
|
||||
{
|
||||
let txn = self.lmdb.read_txn()?;
|
||||
|
||||
// We insert into a BTreeSet to keep them time-ordered
|
||||
let mut output: BTreeSet<Event> = BTreeSet::new();
|
||||
|
||||
if filter.num_ids() > 0 {
|
||||
// Fetch by id
|
||||
for id in filter.ids() {
|
||||
// Stop if limited
|
||||
if output.len() >= filter.limit() as usize {
|
||||
break;
|
||||
}
|
||||
if let Some(event) = self.get_event_by_id(id)? {
|
||||
// and check each against the rest of the filter
|
||||
if filter.event_matches(&event)? && screen(&event) {
|
||||
output.insert(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if filter.num_authors() > 0 && filter.num_kinds() > 0 {
|
||||
// We may bring since forward if we hit the limit without going back that
|
||||
// far, so we use a mutable since:
|
||||
let mut since = filter.since();
|
||||
|
||||
for author in filter.authors() {
|
||||
for kind in filter.kinds() {
|
||||
let iter = self
|
||||
.lmdb
|
||||
.akc_iter(author, kind, since, filter.until(), &txn)?;
|
||||
|
||||
// Count how many we have found of this author-kind pair, so we
|
||||
// can possibly update `since`
|
||||
let mut paircount = 0;
|
||||
|
||||
'per_event: for result in iter {
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
// If we have gone beyond since, we can stop early
|
||||
// (We have to check because `since` might change in this loop)
|
||||
if event.created_at() < since {
|
||||
break 'per_event;
|
||||
}
|
||||
|
||||
// check against the rest of the filter
|
||||
if filter.event_matches(&event)? && screen(&event) {
|
||||
// Accept the event
|
||||
output.insert(event);
|
||||
paircount += 1;
|
||||
|
||||
// Stop this pair if limited
|
||||
if paircount >= filter.limit() as usize {
|
||||
// Since we found the limit just among this pair,
|
||||
// potentially move since forward
|
||||
if event.created_at() > since {
|
||||
since = event.created_at();
|
||||
}
|
||||
break 'per_event;
|
||||
}
|
||||
|
||||
// If kind is replaceable (and not parameterized)
|
||||
// then don't take any more events for this author-kind
|
||||
// pair.
|
||||
// NOTE that this optimization is difficult to implement
|
||||
// for other replaceable event situations
|
||||
if kind.is_replaceable() {
|
||||
break 'per_event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if filter.num_authors() > 0 && !filter.tags()?.is_empty() {
|
||||
// We may bring since forward if we hit the limit without going back that
|
||||
// far, so we use a mutable since:
|
||||
let mut since = filter.since();
|
||||
|
||||
for author in filter.authors() {
|
||||
let tags = filter.tags()?;
|
||||
for mut tag in tags.iter() {
|
||||
if let Some(tag0) = tag.next() {
|
||||
if let Some(tagvalue) = tag.next() {
|
||||
let iter = self.lmdb.atc_iter(
|
||||
author,
|
||||
tag0[0],
|
||||
tagvalue,
|
||||
since,
|
||||
filter.until(),
|
||||
&txn,
|
||||
)?;
|
||||
|
||||
// Count how many we have found of this author-tag pair, so we
|
||||
// can possibly update `since`
|
||||
let mut paircount = 0;
|
||||
|
||||
'per_event: for result in iter {
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
// If we have gone beyond since, we can stop early
|
||||
// (We have to check because `since` might change in this loop)
|
||||
if event.created_at() < since {
|
||||
break 'per_event;
|
||||
}
|
||||
|
||||
// check against the rest of the filter
|
||||
if filter.event_matches(&event)? && screen(&event) {
|
||||
// Accept the event
|
||||
output.insert(event);
|
||||
paircount += 1;
|
||||
|
||||
// Stop this pair if limited
|
||||
if paircount >= filter.limit() as usize {
|
||||
// Since we found the limit just among this pair,
|
||||
// potentially move since forward
|
||||
if event.created_at() > since {
|
||||
since = event.created_at();
|
||||
}
|
||||
break 'per_event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if filter.num_kinds() > 0 && !filter.tags()?.is_empty() {
|
||||
// We may bring since forward if we hit the limit without going back that
|
||||
// far, so we use a mutable since:
|
||||
let mut since = filter.since();
|
||||
|
||||
for kind in filter.kinds() {
|
||||
let tags = filter.tags()?;
|
||||
for mut tag in tags.iter() {
|
||||
if let Some(tag0) = tag.next() {
|
||||
if let Some(tagvalue) = tag.next() {
|
||||
let iter = self.lmdb.ktc_iter(
|
||||
kind,
|
||||
tag0[0],
|
||||
tagvalue,
|
||||
since,
|
||||
filter.until(),
|
||||
&txn,
|
||||
)?;
|
||||
|
||||
// Count how many we have found of this kind-tag pair, so we
|
||||
// can possibly update `since`
|
||||
let mut paircount = 0;
|
||||
|
||||
'per_event: for result in iter {
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
// If we have gone beyond since, we can stop early
|
||||
// (We have to check because `since` might change in this loop)
|
||||
if event.created_at() < since {
|
||||
break 'per_event;
|
||||
}
|
||||
|
||||
// check against the rest of the filter
|
||||
if filter.event_matches(&event)? && screen(&event) {
|
||||
// Accept the event
|
||||
output.insert(event);
|
||||
paircount += 1;
|
||||
|
||||
// Stop this pair if limited
|
||||
if paircount >= filter.limit() as usize {
|
||||
// Since we found the limit just among this pair,
|
||||
// potentially move since forward
|
||||
if event.created_at() > since {
|
||||
since = event.created_at();
|
||||
}
|
||||
break 'per_event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !filter.tags()?.is_empty() {
|
||||
// We may bring since forward if we hit the limit without going back that
|
||||
// far, so we use a mutable since:
|
||||
let mut since = filter.since();
|
||||
|
||||
let tags = filter.tags()?;
|
||||
for mut tag in tags.iter() {
|
||||
if let Some(tag0) = tag.next() {
|
||||
if let Some(tagvalue) = tag.next() {
|
||||
let iter =
|
||||
self.lmdb
|
||||
.tc_iter(tag0[0], tagvalue, since, filter.until(), &txn)?;
|
||||
|
||||
let mut rangecount = 0;
|
||||
|
||||
'per_event: for result in iter {
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
if event.created_at() < since {
|
||||
break 'per_event;
|
||||
}
|
||||
|
||||
// check against the rest of the filter
|
||||
if filter.event_matches(&event)? && screen(&event) {
|
||||
// Accept the event
|
||||
output.insert(event);
|
||||
rangecount += 1;
|
||||
|
||||
// Stop this limited
|
||||
if rangecount >= filter.limit() as usize {
|
||||
if event.created_at() > since {
|
||||
since = event.created_at();
|
||||
}
|
||||
break 'per_event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if filter.num_authors() > 0 {
|
||||
// We may bring since forward if we hit the limit without going back that
|
||||
// far, so we use a mutable since:
|
||||
let mut since = filter.since();
|
||||
|
||||
for author in filter.authors() {
|
||||
let iter = self.lmdb.ac_iter(author, since, filter.until(), &txn)?;
|
||||
|
||||
let mut rangecount = 0;
|
||||
|
||||
'per_event: for result in iter {
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
if event.created_at() < filter.since() {
|
||||
break 'per_event;
|
||||
}
|
||||
|
||||
// check against the rest of the filter
|
||||
if filter.event_matches(&event)? && screen(&event) {
|
||||
// Accept the event
|
||||
output.insert(event);
|
||||
rangecount += 1;
|
||||
|
||||
// Stop this limited
|
||||
if rangecount >= filter.limit() as usize {
|
||||
if event.created_at() > since {
|
||||
since = event.created_at();
|
||||
}
|
||||
break 'per_event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// SCRAPE:
|
||||
let maxtime = filter.until().0.min(Time::now().0);
|
||||
|
||||
let allow = config.allow_scraping
|
||||
|| filter.limit() <= config.allow_scrape_if_limited_to
|
||||
|| (maxtime - filter.since().0) < config.allow_scrape_if_max_seconds;
|
||||
if !allow {
|
||||
return Err(ChorusError::Scraper.into());
|
||||
}
|
||||
|
||||
// This is INEFFICIENT as it scans through many events
|
||||
|
||||
let iter = self.lmdb.ci_iter(filter.since(), filter.until(), &txn)?;
|
||||
for result in iter {
|
||||
if output.len() >= filter.limit() as usize {
|
||||
break;
|
||||
}
|
||||
let (_key, offset) = result?;
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
if filter.event_matches(&event)? && screen(&event) {
|
||||
output.insert(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to a Vec, reverse time order, and apply limit
|
||||
Ok(output
|
||||
.iter()
|
||||
.rev()
|
||||
.take(filter.limit() as usize)
|
||||
.copied()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Delete an event by id.
|
||||
///
|
||||
/// This deindexes the event.
|
||||
///
|
||||
/// This does not add to the deleted_ids record, which is for events
|
||||
/// that are deleted by other events
|
||||
fn delete_by_id(&self, txn: &mut RwTxn<'_>, id: Id) -> Result<(), Error> {
|
||||
if let Some(offset) = self.lmdb.get_offset_by_id(txn, id)? {
|
||||
self.delete_by_offset(txn, offset)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an event by offset.
|
||||
///
|
||||
/// This deindexes the event.
|
||||
///
|
||||
/// This does not add to the deleted_ids record, which is for events
|
||||
/// that are deleted by other events
|
||||
fn delete_by_offset(&self, txn: &mut RwTxn<'_>, offset: usize) -> Result<(), Error> {
|
||||
// Get event
|
||||
let event = self.events.get_event_by_offset(offset)?;
|
||||
|
||||
// Remove from indexes
|
||||
self.lmdb.deindex(txn, &event)?;
|
||||
|
||||
// Also remove from the id index
|
||||
self.lmdb.deindex_id(txn, event.id())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This deletes an event without marking it as having been deleted by another event
|
||||
pub fn delete_event(&self, id: Id) -> Result<(), Error> {
|
||||
let mut txn = self.lmdb.write_txn()?;
|
||||
self.delete_by_id(&mut txn, id)?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// If the event is replaceable or parameterized replaceable
|
||||
// this deletes all the events in that group except the most recent one.
|
||||
fn delete_replaced(&self, txn: &mut RwTxn<'_>, event: &Event) -> Result<(), Error> {
|
||||
if event.kind().is_replaceable() {
|
||||
let loop_txn = self.lmdb.read_txn()?;
|
||||
let iter = self.lmdb.akc_iter(
|
||||
event.pubkey(),
|
||||
event.kind(),
|
||||
Time::min(),
|
||||
Time::max(),
|
||||
&loop_txn,
|
||||
)?;
|
||||
let mut first = true;
|
||||
for result in iter {
|
||||
// Keep the first result
|
||||
if first {
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let (_key, offset) = result?;
|
||||
|
||||
// Delete the event
|
||||
self.delete_by_offset(txn, offset)?;
|
||||
}
|
||||
} else if event.kind().is_parameterized_replaceable() {
|
||||
let tags = event.tags()?;
|
||||
if let Some(identifier) = tags.get_value(b"d") {
|
||||
let loop_txn = self.lmdb.read_txn()?;
|
||||
let iter = self.lmdb.atc_iter(
|
||||
event.pubkey(),
|
||||
b'd',
|
||||
identifier,
|
||||
Time::min(),
|
||||
Time::max(),
|
||||
&loop_txn,
|
||||
)?;
|
||||
let mut first = true;
|
||||
for result in iter {
|
||||
// Keep the first result
|
||||
if first {
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let (_key, offset) = result?;
|
||||
|
||||
// Delete the event
|
||||
self.delete_by_offset(txn, offset)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_ip_data(&self, ip: HashedIp) -> Result<IpData, Error> {
|
||||
self.lmdb.get_ip_data(ip)
|
||||
}
|
||||
|
||||
pub fn update_ip_data(&self, ip: HashedIp, data: &IpData) -> Result<(), Error> {
|
||||
self.lmdb.update_ip_data(ip, data)
|
||||
}
|
||||
|
||||
pub fn mark_event_approval(&self, id: Id, approval: bool) -> Result<(), Error> {
|
||||
self.lmdb.mark_event_approval(id, approval)
|
||||
}
|
||||
|
||||
pub fn clear_event_approval(&self, id: Id) -> Result<(), Error> {
|
||||
self.lmdb.clear_event_approval(id)
|
||||
}
|
||||
|
||||
pub fn get_event_approval(&self, id: Id) -> Result<Option<bool>, Error> {
|
||||
self.lmdb.get_event_approval(id)
|
||||
}
|
||||
|
||||
pub fn dump_event_approvals(&self) -> Result<Vec<(Id, bool)>, Error> {
|
||||
self.lmdb.dump_event_approvals()
|
||||
}
|
||||
|
||||
pub fn mark_pubkey_approval(&self, pubkey: Pubkey, approval: bool) -> Result<(), Error> {
|
||||
self.lmdb.mark_pubkey_approval(pubkey, approval)
|
||||
}
|
||||
|
||||
pub fn clear_pubkey_approval(&self, pubkey: Pubkey) -> Result<(), Error> {
|
||||
self.lmdb.clear_pubkey_approval(pubkey)
|
||||
}
|
||||
|
||||
pub fn get_pubkey_approval(&self, pubkey: Pubkey) -> Result<Option<bool>, Error> {
|
||||
self.lmdb.get_pubkey_approval(pubkey)
|
||||
}
|
||||
|
||||
pub fn dump_pubkey_approvals(&self) -> Result<Vec<(Pubkey, bool)>, Error> {
|
||||
self.lmdb.dump_pubkey_approvals()
|
||||
}
|
||||
}
|
||||
@ -1,318 +0,0 @@
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::types::parse::json_parse::*;
|
||||
|
||||
/// Parses a JSON event from the `input` buffer. Places the parsed event into the `output` buffer.
|
||||
/// Returns the count of consumed bytes and output bytes
|
||||
pub fn parse_json_event(input: &[u8], output: &mut [u8]) -> Result<(usize, usize), Error> {
|
||||
// Minimum-sized JSON event is 204 characters long
|
||||
if input.len() < 204 {
|
||||
return Err(ChorusError::JsonBadEvent("Too Short", 0).into());
|
||||
}
|
||||
|
||||
// NOTE: 152 is the minimum binary event
|
||||
if output.len() < 152 {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
|
||||
// This tracks where we are currently looking in the input as we scan forward.
|
||||
// It is short for INput POSition.
|
||||
let mut inpos = 0;
|
||||
|
||||
// If tags comes before content, content can use this to know where to put itself.
|
||||
// This is the length of the tags output section. 0 means it hasn't been written yet.
|
||||
let mut tags_size: usize = 0;
|
||||
|
||||
// If content comes before tags, we cannot write it because we don't know how much
|
||||
// space Tags will take. So we instead just remember where the content string
|
||||
// begins so we can write it later.
|
||||
let mut content_input_start: usize = 0;
|
||||
|
||||
// Remember which fields we have read using bit flags.
|
||||
// We must get all seven of these fields for an event to be valid.
|
||||
const HAVE_ID: u8 = 0x1 << 0;
|
||||
const HAVE_PUBKEY: u8 = 0x1 << 1;
|
||||
const HAVE_SIG: u8 = 0x1 << 2;
|
||||
const HAVE_CREATED_AT: u8 = 0x1 << 3;
|
||||
const HAVE_KIND: u8 = 0x1 << 4;
|
||||
const HAVE_CONTENT: u8 = 0x1 << 5;
|
||||
const HAVE_TAGS: u8 = 0x1 << 6;
|
||||
let mut complete: u8 = 0;
|
||||
|
||||
eat_whitespace(input, &mut inpos);
|
||||
verify_char(input, b'{', &mut inpos)?;
|
||||
loop {
|
||||
eat_whitespace(input, &mut inpos);
|
||||
|
||||
// Presuming that we must have at least one field, we don't have to look
|
||||
// for the end of the object yet.
|
||||
|
||||
// Move to the start of the field name
|
||||
verify_char(input, b'"', &mut inpos)?;
|
||||
|
||||
// No matter which field is next, we need at least 7 bytes for the smallest
|
||||
// field and value: kind":1
|
||||
// This allows us to skip length tests below that are shorter than inpos+7
|
||||
if inpos + 7 > input.len() {
|
||||
return Err(ChorusError::JsonBadEvent("Too Short or Missing Fields", inpos).into());
|
||||
}
|
||||
|
||||
if &input[inpos..inpos + 3] == b"id\"" {
|
||||
if complete & HAVE_ID == HAVE_ID {
|
||||
return Err(ChorusError::JsonBadEvent("Duplicate id field", inpos).into());
|
||||
}
|
||||
inpos += 3;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
read_id(input, &mut inpos, &mut output[16..48])?;
|
||||
complete |= HAVE_ID;
|
||||
} else if &input[inpos..inpos + 4] == b"sig\"" {
|
||||
if complete & HAVE_SIG == HAVE_SIG {
|
||||
return Err(ChorusError::JsonBadEvent("Duplicate sig field", inpos).into());
|
||||
}
|
||||
inpos += 4;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
read_sig(input, &mut inpos, output)?;
|
||||
complete |= HAVE_SIG;
|
||||
} else if &input[inpos..inpos + 5] == b"kind\"" {
|
||||
if complete & HAVE_KIND == HAVE_KIND {
|
||||
return Err(ChorusError::JsonBadEvent("Duplicate kind field", inpos).into());
|
||||
}
|
||||
inpos += 5;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
let kind = read_kind(input, &mut inpos)?;
|
||||
output[4..6].copy_from_slice(kind.to_ne_bytes().as_slice());
|
||||
complete |= HAVE_KIND;
|
||||
} else if &input[inpos..inpos + 5] == b"tags\"" {
|
||||
if complete & HAVE_TAGS == HAVE_TAGS {
|
||||
return Err(ChorusError::JsonBadEvent("Duplicate tags field", inpos).into());
|
||||
}
|
||||
inpos += 5;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
tags_size = read_tags_array(input, &mut inpos, &mut output[144..])?;
|
||||
complete |= HAVE_TAGS;
|
||||
if content_input_start != 0 {
|
||||
// Content was found earlier than tags.
|
||||
// Now that tags have been read, we should read the content
|
||||
read_content(input, &mut content_input_start, output, 144 + tags_size)?;
|
||||
complete |= HAVE_CONTENT;
|
||||
}
|
||||
} else if &input[inpos..inpos + 7] == b"pubkey\"" {
|
||||
if complete & HAVE_PUBKEY == HAVE_PUBKEY {
|
||||
return Err(ChorusError::JsonBadEvent("Duplicate pubkey field", inpos).into());
|
||||
}
|
||||
inpos += 7;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
read_pubkey(input, &mut inpos, &mut output[48..80])?;
|
||||
complete |= HAVE_PUBKEY;
|
||||
} else if inpos + 8 <= input.len() && &input[inpos..inpos + 8] == b"content\"" {
|
||||
if complete & HAVE_CONTENT == HAVE_CONTENT {
|
||||
return Err(ChorusError::JsonBadEvent("Duplicate pubkey field", inpos).into());
|
||||
}
|
||||
inpos += 8;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
if tags_size == 0 {
|
||||
// Oops, we haven't read the tags yet. That means we don't yet know where
|
||||
// to place the content. In this case we just remember the offset where
|
||||
// this needs to be done, so we can do this later.
|
||||
content_input_start = inpos;
|
||||
// skip past it so we can read the subsequent fields
|
||||
verify_char(input, b'"', &mut inpos)?;
|
||||
burn_string(input, &mut inpos)?;
|
||||
} else {
|
||||
read_content(input, &mut inpos, output, 144 + tags_size)?;
|
||||
complete |= HAVE_CONTENT;
|
||||
}
|
||||
} else if inpos + 11 <= input.len() && &input[inpos..inpos + 11] == b"created_at\"" {
|
||||
if complete & HAVE_CREATED_AT == HAVE_CREATED_AT {
|
||||
return Err(ChorusError::JsonBadEvent("Duplicate created_at field", inpos).into());
|
||||
}
|
||||
inpos += 11;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
let u = read_u64(input, &mut inpos)?;
|
||||
output[8..16].copy_from_slice(u.to_ne_bytes().as_slice());
|
||||
complete |= HAVE_CREATED_AT;
|
||||
} else {
|
||||
burn_key_and_value(input, &mut inpos)?;
|
||||
}
|
||||
|
||||
// get past the comma, or detect the close brace and exit
|
||||
if next_object_field(input, &mut inpos)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if complete == 0b0111_1111 {
|
||||
Ok((
|
||||
inpos,
|
||||
u32::from_ne_bytes(output[0..4].try_into().unwrap()) as usize,
|
||||
))
|
||||
} else {
|
||||
Err(ChorusError::JsonBadEvent("Missing Fields", inpos).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_event() {
|
||||
if 256_u16.to_ne_bytes() == [1, 0] {
|
||||
test_parse_json_event_big_endian();
|
||||
} else {
|
||||
test_parse_json_event_little_endian();
|
||||
}
|
||||
}
|
||||
|
||||
fn test_parse_json_event_little_endian() {
|
||||
let json = br#"{"id":"a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7","pubkey":"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49","created_at":1681778790,"kind":1,"sig":"4dfea1a6f73141d5691e43afc3234dbe73016db0fb207cf247e0127cc2591ee6b4be5b462272030a9bde75882aae810f359682b1b6ce6cbb97201141c576db42","content":"He got snowed in","tags":[["client","gossip"],["p","e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"],["e","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","wss://nostr-pub.wellorder.net/","root"]]}"#;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer.resize(4096, 0);
|
||||
let (_insize, size) = parse_json_event(&json[..], &mut buffer).unwrap();
|
||||
assert_eq!(size, 372);
|
||||
assert_eq!(
|
||||
&buffer[0..size],
|
||||
&[
|
||||
116, 1, 0, 0, // 372 bytes long
|
||||
1, 0, // kind 1
|
||||
0, 0, // padding
|
||||
102, 232, 61, 100, 0, 0, 0, 0, // created at 1681778790
|
||||
169, 102, 48, 85, 22, 74, 184, 179, 13, 149, 36, 101, 99, 112, 196, 191, 147, 57,
|
||||
59, 176, 81, 183, 237, 244, 85, 111, 64, 197, 41, 141, 192, 199, // id
|
||||
238, 17, 165, 223, 244, 12, 25, 165, 85, 244, 31, 228, 43, 72, 240, 14, 97, 140,
|
||||
145, 34, 86, 34, 174, 55, 182, 194, 187, 103, 183, 108, 78, 73, // pubkey
|
||||
77, 254, 161, 166, 247, 49, 65, 213, 105, 30, 67, 175, 195, 35, 77, 190, 115, 1,
|
||||
109, 176, 251, 32, 124, 242, 71, 224, 18, 124, 194, 89, 30, 230, 180, 190, 91, 70,
|
||||
34, 114, 3, 10, 155, 222, 117, 136, 42, 174, 129, 15, 53, 150, 130, 177, 182, 206,
|
||||
108, 187, 151, 32, 17, 65, 197, 118, 219, 66, // sig
|
||||
// 144:
|
||||
208, 0, // tags section is 208 bytes long
|
||||
3, 0, // there are three tags
|
||||
10, 0, // first tag is at offset 10
|
||||
28, 0, // second tag is at offset 28
|
||||
99, 0, // third tag is at offset 99
|
||||
// 154: (144+10)
|
||||
2, 0, // the first tag has 2 strings
|
||||
6, 0, // the first string is 6 bytes long
|
||||
99, 108, 105, 101, 110, 116, // "client"
|
||||
6, 0, // the second string is 6 bytes long
|
||||
103, 111, 115, 115, 105, 112, // "gossip"
|
||||
// 172: (144+28)
|
||||
2, 0, // the second tag has two strings
|
||||
1, 0, // the first string is 1 char long
|
||||
112, // "p"
|
||||
64, 0, // the second string is 64 bytes long
|
||||
101, 50, 99, 99, 102, 55, 99, 102, 50, 48, 52, 48, 51, 102, 51, 102, 50, 97, 52,
|
||||
97, 53, 53, 98, 51, 50, 56, 102, 48, 100, 101, 51, 98, 101, 51, 56, 53, 53, 56, 97,
|
||||
55, 100, 53, 102, 51, 51, 54, 51, 50, 102, 100, 97, 97, 101, 102, 99, 55, 50, 54,
|
||||
99, 49, 99, 56, 101,
|
||||
98, // "e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"
|
||||
// 243: (144+99)
|
||||
4, 0, // the third tag has 4 strings
|
||||
1, 0, // the first string is 1 char long
|
||||
101, // "e"
|
||||
64, 0, // the second string is 64 bytes long
|
||||
50, 99, 56, 54, 97, 98, 99, 99, 57, 56, 102, 55, 102, 100, 56, 97, 54, 55, 53, 48,
|
||||
97, 97, 98, 56, 100, 102, 54, 99, 49, 56, 54, 51, 57, 48, 51, 102, 49, 48, 55, 50,
|
||||
48, 54, 99, 99, 50, 100, 55, 50, 101, 56, 97, 102, 101, 98, 54, 99, 51, 56, 51, 53,
|
||||
55, 97, 101,
|
||||
100, // "2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"
|
||||
30, 0, // the third string is 30 bytes long
|
||||
119, 115, 115, 58, 47, 47, 110, 111, 115, 116, 114, 45, 112, 117, 98, 46, 119, 101,
|
||||
108, 108, 111, 114, 100, 101, 114, 46, 110, 101, 116,
|
||||
47, // "wss://nostr-pub.wellorder.net/"
|
||||
4, 0, // the fourth string is 4 bytes long
|
||||
114, 111, 111, 116, // "root"
|
||||
// 352: (144+208)
|
||||
16, 0, 0, 0, // the content is 16 bytes long
|
||||
72, 101, 32, 103, 111, 116, 32, 115, 110, 111, 119, 101, 100, 32, 105,
|
||||
110, // "He got snowed in"
|
||||
|
||||
// 372:
|
||||
]
|
||||
);
|
||||
|
||||
// Same event in a different order
|
||||
let json2 = br#"{"kind":1,"pubkey":"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49","created_at":1681778790,"sig":"4dfea1a6f73141d5691e43afc3234dbe73016db0fb207cf247e0127cc2591ee6b4be5b462272030a9bde75882aae810f359682b1b6ce6cbb97201141c576db42","tags":[["client","gossip"],["p","e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"],["e","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","wss://nostr-pub.wellorder.net/","root"]],"id":"a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7","content":"He got snowed in"}"#;
|
||||
let mut buffer2: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer2.resize(4096, 0);
|
||||
let (_insize, size) = parse_json_event(&json2[..], &mut buffer2).unwrap();
|
||||
assert_eq!(size, 372);
|
||||
assert_eq!(&buffer[..372], &buffer2[..372]);
|
||||
}
|
||||
|
||||
fn test_parse_json_event_big_endian() {
|
||||
let json = br#"{"id":"a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7","pubkey":"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49","created_at":1681778790,"kind":1,"sig":"4dfea1a6f73141d5691e43afc3234dbe73016db0fb207cf247e0127cc2591ee6b4be5b462272030a9bde75882aae810f359682b1b6ce6cbb97201141c576db42","content":"He got snowed in","tags":[["client","gossip"],["p","e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"],["e","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","wss://nostr-pub.wellorder.net/","root"]]}"#;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer.resize(4096, 0);
|
||||
let (_insize, size) = parse_json_event(&json[..], &mut buffer).unwrap();
|
||||
assert_eq!(size, 372);
|
||||
assert_eq!(
|
||||
&buffer[0..size],
|
||||
&[
|
||||
0, 0, 1, 116, // 372 bytes long
|
||||
0, 1, // kind 1
|
||||
0, 0, // padding
|
||||
0, 0, 0, 0, 100, 61, 232, 102, // created at 1681778790
|
||||
169, 102, 48, 85, 22, 74, 184, 179, 13, 149, 36, 101, 99, 112, 196, 191, 147, 57,
|
||||
59, 176, 81, 183, 237, 244, 85, 111, 64, 197, 41, 141, 192, 199, // id
|
||||
238, 17, 165, 223, 244, 12, 25, 165, 85, 244, 31, 228, 43, 72, 240, 14, 97, 140,
|
||||
145, 34, 86, 34, 174, 55, 182, 194, 187, 103, 183, 108, 78, 73, // pubkey
|
||||
77, 254, 161, 166, 247, 49, 65, 213, 105, 30, 67, 175, 195, 35, 77, 190, 115, 1,
|
||||
109, 176, 251, 32, 124, 242, 71, 224, 18, 124, 194, 89, 30, 230, 180, 190, 91, 70,
|
||||
34, 114, 3, 10, 155, 222, 117, 136, 42, 174, 129, 15, 53, 150, 130, 177, 182, 206,
|
||||
108, 187, 151, 32, 17, 65, 197, 118, 219, 66, // sig
|
||||
// 144:
|
||||
0, 208, // tags section is 208 bytes long
|
||||
3, 0, // there are three tags
|
||||
0, 10, // first tag is at offset 10
|
||||
0, 28, // second tag is at offset 28
|
||||
0, 99, // third tag is at offset 99
|
||||
// 154: (144+10)
|
||||
0, 2, // the first tag has 2 strings
|
||||
0, 6, // the first string is 6 bytes long
|
||||
99, 108, 105, 101, 110, 116, // "client"
|
||||
0, 6, // the second string is 6 bytes long
|
||||
103, 111, 115, 115, 105, 112, // "gossip"
|
||||
// 172: (144+28)
|
||||
0, 2, // the second tag has two strings
|
||||
0, 1, // the first string is 1 char long
|
||||
112, // "p"
|
||||
0, 64, // the second string is 64 bytes long
|
||||
101, 50, 99, 99, 102, 55, 99, 102, 50, 48, 52, 48, 51, 102, 51, 102, 50, 97, 52,
|
||||
97, 53, 53, 98, 51, 50, 56, 102, 48, 100, 101, 51, 98, 101, 51, 56, 53, 53, 56, 97,
|
||||
55, 100, 53, 102, 51, 51, 54, 51, 50, 102, 100, 97, 97, 101, 102, 99, 55, 50, 54,
|
||||
99, 49, 99, 56, 101,
|
||||
98, // "e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"
|
||||
// 243: (144+99)
|
||||
0, 4, // the third tag has 4 strings
|
||||
0, 1, // the first string is 1 char long
|
||||
101, // "e"
|
||||
0, 64, // the second string is 64 bytes long
|
||||
50, 99, 56, 54, 97, 98, 99, 99, 57, 56, 102, 55, 102, 100, 56, 97, 54, 55, 53, 48,
|
||||
97, 97, 98, 56, 100, 102, 54, 99, 49, 56, 54, 51, 57, 48, 51, 102, 49, 48, 55, 50,
|
||||
48, 54, 99, 99, 50, 100, 55, 50, 101, 56, 97, 102, 101, 98, 54, 99, 51, 56, 51, 53,
|
||||
55, 97, 101,
|
||||
100, // "2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"
|
||||
0, 30, // the third string is 30 bytes long
|
||||
119, 115, 115, 58, 47, 47, 110, 111, 115, 116, 114, 45, 112, 117, 98, 46, 119, 101,
|
||||
108, 108, 111, 114, 100, 101, 114, 46, 110, 101, 116,
|
||||
47, // "wss://nostr-pub.wellorder.net/"
|
||||
0, 4, // the fourth string is 4 bytes long
|
||||
114, 111, 111, 116, // "root"
|
||||
// 352: (144+208)
|
||||
0, 0, 0, 16, // the content is 16 bytes long
|
||||
72, 101, 32, 103, 111, 116, 32, 115, 110, 111, 119, 101, 100, 32, 105,
|
||||
110, // "He got snowed in"
|
||||
// 372:
|
||||
]
|
||||
);
|
||||
|
||||
// Same event in a different order
|
||||
let json2 = br#"{"kind":1,"pubkey":"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49","created_at":1681778790,"sig":"4dfea1a6f73141d5691e43afc3234dbe73016db0fb207cf247e0127cc2591ee6b4be5b462272030a9bde75882aae810f359682b1b6ce6cbb97201141c576db42","tags":[["client","gossip"],["p","e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"],["e","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","wss://nostr-pub.wellorder.net/","root"]],"id":"a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7","content":"He got snowed in"}"#;
|
||||
let mut buffer2: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer2.resize(4096, 0);
|
||||
let (_insize, size) = parse_json_event(&json2[..], &mut buffer2).unwrap();
|
||||
assert_eq!(size, 372);
|
||||
assert_eq!(&buffer[..372], &buffer2[..372]);
|
||||
}
|
||||
}
|
||||
@ -1,243 +0,0 @@
|
||||
use super::{Id, Kind, Pubkey, Sig, Tags, Time};
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::types::parse::json_escape::json_escape;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
|
||||
mod json_event;
|
||||
use json_event::parse_json_event;
|
||||
|
||||
/*
|
||||
* 0 [4 bytes] length of the event structure
|
||||
* 4 [2 bytes] kind
|
||||
* 6 [2 bytes] PADDING
|
||||
* 8 [8 bytes] created_at
|
||||
* 16 [32 bytes] id
|
||||
* 48 [32 bytes] pubkey
|
||||
* 80 [64 bytes] sig
|
||||
* 144 [T bytes] Tags
|
||||
* 144+T [4 bytes] content length
|
||||
* 144+T+4 [C bytes] content
|
||||
* 144+T+4+C <--- beginning of region beyond the event
|
||||
*/
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Event<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> Event<'a> {
|
||||
// Parse json into an Event. Returns the count of consumed input bytes and the Event
|
||||
pub fn from_json(
|
||||
json: &[u8],
|
||||
output_buffer: &'a mut [u8],
|
||||
) -> Result<(usize, Event<'a>), Error> {
|
||||
let (incount, outcount) = parse_json_event(json, output_buffer)?;
|
||||
Ok((incount, Event(&output_buffer[..outcount])))
|
||||
}
|
||||
|
||||
// this marks off the slice of bytes that represent an event from a potentially longer input
|
||||
pub fn delineate(input: &'a [u8]) -> Result<Event<'a>, Error> {
|
||||
if input.len() < 144 + 4 + 4 {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
let len = parse_u32!(input, 0) as usize;
|
||||
if input.len() < len {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
Ok(Event(&input[0..len]))
|
||||
}
|
||||
|
||||
// This copies
|
||||
pub fn copy(&self, output: &mut [u8]) -> Result<(), Error> {
|
||||
if output.len() < self.0.len() {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
output[..self.0.len()].copy_from_slice(self.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This copies, using the event_store mmap-append api
|
||||
pub fn macopy(&self, output: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||
if output.len() < self.0.len() {
|
||||
return Err(std::io::Error::other(ChorusError::BufferTooSmall));
|
||||
}
|
||||
output[..self.0.len()].copy_from_slice(self.0);
|
||||
Ok(self.0.len())
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn length(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> Kind {
|
||||
Kind(parse_u16!(self.0, 4))
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> Time {
|
||||
Time(parse_u64!(self.0, 8))
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Id {
|
||||
Id(self.0[16..16 + 32].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn pubkey(&self) -> Pubkey {
|
||||
Pubkey(self.0[48..48 + 32].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn sig(&self) -> Sig {
|
||||
Sig(self.0[80..80 + 64].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn tags(&'a self) -> Result<Tags<'a>, Error> {
|
||||
Tags::delineate(&self.0[144..])
|
||||
}
|
||||
|
||||
pub fn content(&'a self) -> &'a [u8] {
|
||||
let t = parse_u16!(self.0, 144) as usize;
|
||||
let c = parse_u32!(self.0, 144 + t) as usize;
|
||||
&self.0[144 + t + 4..144 + t + 4 + c]
|
||||
}
|
||||
|
||||
pub fn as_json(&self) -> Result<Vec<u8>, Error> {
|
||||
let mut output: Vec<u8> = Vec::with_capacity(256);
|
||||
output.extend(br#"{"id":""#);
|
||||
let pos = output.len();
|
||||
output.resize(pos + 64, 0);
|
||||
self.id().write_hex(&mut output[pos..]).unwrap();
|
||||
output.extend(br#"","pubkey":""#);
|
||||
let pos = output.len();
|
||||
output.resize(pos + 64, 0);
|
||||
self.pubkey().write_hex(&mut output[pos..]).unwrap();
|
||||
output.extend(br#"","kind":"#);
|
||||
output.extend(format!("{}", self.kind().0).as_bytes());
|
||||
output.extend(br#","created_at":"#);
|
||||
output.extend(format!("{}", self.created_at().0).as_bytes());
|
||||
output.extend(br#","tags":"#);
|
||||
output.extend(self.tags()?.as_json());
|
||||
output.extend(br#","content":""#);
|
||||
// This is okay if it is not accurate. It generally avoids
|
||||
// lots of little mallocs when the capacity is already allocated
|
||||
output.reserve(self.content().len() * 7 / 6);
|
||||
let mut output = json_escape(self.content(), output)?;
|
||||
output.extend(br#"","sig":""#);
|
||||
let pos = output.len();
|
||||
output.resize(pos + 128, 0);
|
||||
self.sig().write_hex(&mut output[pos..]).unwrap();
|
||||
output.extend(br#""}"#);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<(), Error> {
|
||||
use secp256k1::hashes::{sha256, Hash};
|
||||
use secp256k1::schnorr::Signature;
|
||||
use secp256k1::{Message, XOnlyPublicKey};
|
||||
|
||||
// This is okay if it is not accurate. It generally avoids
|
||||
// lots of little mallocs when the capacity is already allocated
|
||||
let escaped_content = Vec::with_capacity(self.content().len() * 7 / 6);
|
||||
let escaped_content = json_escape(self.content(), escaped_content)?;
|
||||
|
||||
let signable = format!(
|
||||
r#"[0,"{}",{},{},{},"{}"]"#,
|
||||
self.pubkey(),
|
||||
self.created_at(),
|
||||
self.kind(),
|
||||
self.tags()?,
|
||||
unsafe { std::str::from_utf8_unchecked(&escaped_content[..]) },
|
||||
);
|
||||
|
||||
drop(escaped_content);
|
||||
|
||||
let hash = sha256::Hash::hash(signable.as_bytes());
|
||||
|
||||
let hashref = <sha256::Hash as AsRef<[u8]>>::as_ref(&hash);
|
||||
if hashref != self.id().as_slice() {
|
||||
return Err(ChorusError::BadEventId.into());
|
||||
}
|
||||
|
||||
let pubkey = XOnlyPublicKey::from_slice(self.pubkey().as_slice())?;
|
||||
let sig = Signature::from_slice(self.sig().as_slice())?;
|
||||
let message = Message::from_digest_slice(hashref)?;
|
||||
sig.verify(&message, &pubkey)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> Result<bool, Error> {
|
||||
for mut tag in self.tags()?.iter() {
|
||||
if tag.next() == Some(b"expiration") {
|
||||
if let Some(expires) = tag.next() {
|
||||
// Interpret string as a u64
|
||||
let mut p = 0;
|
||||
let time = super::parse::json_parse::read_u64(expires, &mut p)?;
|
||||
if time <= Time::now().0 {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Event<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Ok(bytes) = self.as_json() {
|
||||
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
write!(f, "{s}")
|
||||
} else {
|
||||
write!(f, "{{Corrupted Event}}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Event<'_> {}
|
||||
|
||||
impl PartialOrd for Event<'_> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Event<'_> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.created_at()
|
||||
.cmp(&other.created_at())
|
||||
.then(self.id().cmp(&other.id()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OwnedEvent(pub Vec<u8>);
|
||||
|
||||
impl OwnedEvent {
|
||||
pub fn as_event(&self) -> Result<Event<'_>, Error> {
|
||||
Event::delineate(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Event;
|
||||
|
||||
#[test]
|
||||
fn test_event_expired() {
|
||||
let json = br#"{"id":"8b8b1d98f279b43f571ce55dce7cc51ced0c24e9558bfdaa0be0467f82f64708","pubkey":"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49","created_at":1712693549,"kind":1,"sig":"870497b1a254f2394a692decd46b5cffa044302179a42e985697b488fc408118c9ff7c5578d85393474c1a025f28c869148968ee3229aa24425800ae54f54e51","content":"He got snowed in","tags":[["expiration","1712693529"],["p","e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"],["e","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","wss://nostr-pub.wellorder.net/","root"]]}"#;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer.resize(4096, 0);
|
||||
let (_size, event) = Event::from_json(json.as_slice(), &mut buffer).unwrap();
|
||||
assert_eq!(event.is_expired().unwrap(), true); // In the past
|
||||
|
||||
let json = br#"{"id":"120b3d99f889c6147972b0256413e84b0b7b7862a705964b7302f5392677e52a","pubkey":"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49","created_at":1712693868,"kind":1,"sig":"ed0463b822f76f63c392b00d4a66c297f5e13371c800b139f2d40174bf77146201f29ae6e3a9da71a9346416d8b2ba4d2f5a2be693a9e75a91a33abfdc43ec71","content":"He got snowed in","tags":[["expiration","99712693529"],["p","e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"],["e","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","wss://nostr-pub.wellorder.net/","root"]]}"#;
|
||||
let (_size, event) = Event::from_json(json.as_slice(), &mut buffer).unwrap();
|
||||
assert_eq!(event.is_expired().unwrap(), false); // Too far in the future
|
||||
|
||||
let json = br#"{"kind":1,"pubkey":"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49","created_at":1681778790,"sig":"4dfea1a6f73141d5691e43afc3234dbe73016db0fb207cf247e0127cc2591ee6b4be5b462272030a9bde75882aae810f359682b1b6ce6cbb97201141c576db42","tags":[["client","gossip"],["p","e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"],["e","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","wss://nostr-pub.wellorder.net/","root"]],"id":"a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7","content":"He got snowed in"}"#;
|
||||
let (_size, event) = Event::from_json(json.as_slice(), &mut buffer).unwrap();
|
||||
assert_eq!(event.is_expired().unwrap(), false); // Doesn't have the expiration tag
|
||||
}
|
||||
}
|
||||
@ -1,549 +0,0 @@
|
||||
use super::{
|
||||
ARRAYS_OFFSET, ID_SIZE, KIND_SIZE, LIMIT_OFFSET, NUM_AUTHORS_OFFSET, NUM_IDS_OFFSET,
|
||||
NUM_KINDS_OFFSET, PUBKEY_SIZE, SINCE_OFFSET, UNTIL_OFFSET,
|
||||
};
|
||||
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::types::parse::json_escape::json_unescape;
|
||||
use crate::types::parse::json_parse::*;
|
||||
use crate::types::parse::put;
|
||||
|
||||
/// Parses a JSON filter from the `input` buffer. Places the parsed filter into the `output` buffer.
|
||||
/// Returns the count of consumed bytes and output bytes
|
||||
pub fn parse_json_filter(input: &[u8], output: &mut [u8]) -> Result<(usize, usize), Error> {
|
||||
if input.len() < 2 {
|
||||
return Err(ChorusError::JsonBadFilter("Too short", 0).into());
|
||||
}
|
||||
|
||||
// This tracks where we are currently looking in the input as we scan forward.
|
||||
// It is short for INput POSition.
|
||||
let mut inpos = 0;
|
||||
|
||||
// Remember which fields we have read using bit flags.
|
||||
// We should only get at maximum one of each
|
||||
const HAVE_IDS: u8 = 0x1 << 0;
|
||||
const HAVE_AUTHORS: u8 = 0x1 << 1;
|
||||
const HAVE_KINDS: u8 = 0x1 << 2;
|
||||
const HAVE_LIMIT: u8 = 0x1 << 3;
|
||||
const HAVE_SINCE: u8 = 0x1 << 4;
|
||||
const HAVE_UNTIL: u8 = 0x1 << 5;
|
||||
let mut found: u8 = 0;
|
||||
|
||||
// Remember which tags we have seen
|
||||
// We track A-Z in the lower 26 bits, and a-z in the next 26 bits
|
||||
let mut found_tags: u64 = 0;
|
||||
let letter_to_tag_bit = |letter: u8| -> Option<u64> {
|
||||
match letter {
|
||||
65..=90 => Some(letter as u64 - 65),
|
||||
97..=122 => Some(letter as u64 - 97 + 26),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
// Start structure with that of an empty filter
|
||||
put(
|
||||
output,
|
||||
0,
|
||||
&[
|
||||
0, 0, 0, 0, // length (we will fill it in later)
|
||||
0, 0, // 0 ids
|
||||
0, 0, // 0 authors
|
||||
0, 0, // 0 kinds
|
||||
0, 0, // padding
|
||||
255, 255, 255, 255, // max limit
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // since 1970
|
||||
255, 255, 255, 255, 255, 255, 255, 255, // until max unixtime
|
||||
],
|
||||
)?;
|
||||
|
||||
let mut end: usize = ARRAYS_OFFSET;
|
||||
|
||||
// We just store the position of ids, authors, kinds, and tags
|
||||
// and come back to parse them properly again at the end,
|
||||
// since we need to write them in a particular order.
|
||||
let mut start_ids: Option<usize> = None;
|
||||
let mut start_authors: Option<usize> = None;
|
||||
let mut start_kinds: Option<usize> = None;
|
||||
// Allowing up to 32 tag filter fields (plenty!)
|
||||
// (we are not differentiating letters yet, just collecting offsets)
|
||||
// (we make the array to avoid allocation)
|
||||
let mut num_tag_fields = 0;
|
||||
let mut start_tags: [usize; 32] = [usize::MAX; 32];
|
||||
|
||||
eat_whitespace(input, &mut inpos);
|
||||
verify_char(input, b'{', &mut inpos)?;
|
||||
loop {
|
||||
eat_whitespace_and_commas(input, &mut inpos);
|
||||
|
||||
// Check for end
|
||||
if input[inpos] == b'}' {
|
||||
inpos += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
verify_char(input, b'"', &mut inpos)?;
|
||||
|
||||
if inpos + 4 <= input.len() && &input[inpos..inpos + 4] == b"ids\"" {
|
||||
// Check for duplicate
|
||||
if found & HAVE_IDS == HAVE_IDS {
|
||||
return Err(ChorusError::JsonBadFilter("Duplicate id field", inpos).into());
|
||||
}
|
||||
inpos += 4;
|
||||
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
verify_char(input, b'[', &mut inpos)?;
|
||||
|
||||
// Record for later
|
||||
start_ids = Some(inpos);
|
||||
|
||||
// Burn the field
|
||||
while inpos < input.len() && input[inpos] != b']' {
|
||||
inpos += 1;
|
||||
}
|
||||
verify_char(input, b']', &mut inpos)?;
|
||||
|
||||
// Mark as found 'ids' FIXME this dups `start_ids`
|
||||
found |= HAVE_IDS;
|
||||
} else if inpos + 8 <= input.len() && &input[inpos..inpos + 8] == b"authors\"" {
|
||||
// Check for duplicate
|
||||
if found & HAVE_AUTHORS == HAVE_AUTHORS {
|
||||
return Err(ChorusError::JsonBadFilter("Duplicate authors field", inpos).into());
|
||||
}
|
||||
inpos += 8;
|
||||
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
verify_char(input, b'[', &mut inpos)?;
|
||||
|
||||
// Save the input offset for post-processing
|
||||
start_authors = Some(inpos);
|
||||
|
||||
// Burn the field
|
||||
while inpos < input.len() && input[inpos] != b']' {
|
||||
inpos += 1;
|
||||
}
|
||||
verify_char(input, b']', &mut inpos)?;
|
||||
|
||||
found |= HAVE_AUTHORS;
|
||||
} else if inpos + 6 <= input.len() && &input[inpos..inpos + 6] == b"kinds\"" {
|
||||
// Check for duplicate
|
||||
if found & HAVE_KINDS == HAVE_KINDS {
|
||||
return Err(ChorusError::JsonBadFilter("Duplicate kinds field", inpos).into());
|
||||
}
|
||||
inpos += 6;
|
||||
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
verify_char(input, b'[', &mut inpos)?;
|
||||
|
||||
// Mark this position and bypass this field
|
||||
start_kinds = Some(inpos);
|
||||
|
||||
// Burn the field
|
||||
while inpos < input.len() && input[inpos] != b']' {
|
||||
inpos += 1;
|
||||
}
|
||||
verify_char(input, b']', &mut inpos)?;
|
||||
|
||||
found |= HAVE_KINDS;
|
||||
} else if inpos + 6 <= input.len() && &input[inpos..inpos + 6] == b"since\"" {
|
||||
// Check for duplicate
|
||||
if found & HAVE_SINCE == HAVE_SINCE {
|
||||
return Err(ChorusError::JsonBadFilter("Duplicate since field", inpos).into());
|
||||
}
|
||||
inpos += 6;
|
||||
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
let since = read_u64(input, &mut inpos)?;
|
||||
put(output, SINCE_OFFSET, since.to_ne_bytes().as_slice())?;
|
||||
|
||||
found |= HAVE_SINCE;
|
||||
} else if inpos + 6 <= input.len() && &input[inpos..inpos + 6] == b"until\"" {
|
||||
// Check for duplicate
|
||||
if found & HAVE_UNTIL == HAVE_UNTIL {
|
||||
return Err(ChorusError::JsonBadFilter("Duplicate until field", inpos).into());
|
||||
}
|
||||
inpos += 6;
|
||||
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
let until = read_u64(input, &mut inpos)?;
|
||||
put(output, UNTIL_OFFSET, until.to_ne_bytes().as_slice())?;
|
||||
|
||||
found |= HAVE_UNTIL;
|
||||
} else if inpos + 6 <= input.len() && &input[inpos..inpos + 6] == b"limit\"" {
|
||||
// Check for duplicate
|
||||
if found & HAVE_LIMIT == HAVE_LIMIT {
|
||||
return Err(ChorusError::JsonBadFilter("Duplicate limit field", inpos).into());
|
||||
}
|
||||
inpos += 6;
|
||||
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
let limit = read_u64(input, &mut inpos)?;
|
||||
let limit: u32 = limit as u32;
|
||||
put(output, LIMIT_OFFSET, limit.to_ne_bytes().as_slice())?;
|
||||
|
||||
found |= HAVE_LIMIT;
|
||||
} else if inpos + 3 <= input.len()
|
||||
&& input[inpos] == b'#'
|
||||
&& ((input[inpos + 1] >= 65 && input[inpos + 1] <= 90)
|
||||
|| (input[inpos + 1] >= 97 && input[inpos + 1] <= 122))
|
||||
&& input[inpos + 2] == b'"'
|
||||
{
|
||||
inpos += 1; // pass the hash
|
||||
|
||||
// Mark this position (on the letter itself)
|
||||
start_tags[num_tag_fields] = inpos;
|
||||
num_tag_fields += 1;
|
||||
|
||||
let letter = input[inpos];
|
||||
inpos += 2; // pass the letter and quote
|
||||
|
||||
// Remember we found this tag in the `found_tags` bitfield
|
||||
if let Some(bit) = letter_to_tag_bit(letter) {
|
||||
if found_tags & bit == bit {
|
||||
return Err(ChorusError::JsonBadFilter("Duplicate tag", inpos).into());
|
||||
}
|
||||
found_tags |= bit;
|
||||
}
|
||||
|
||||
// Burn the rest
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
verify_char(input, b'[', &mut inpos)?;
|
||||
burn_array(input, &mut inpos)?;
|
||||
} else {
|
||||
burn_key_and_value(input, &mut inpos)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy ids
|
||||
if let Some(mut inpos) = start_ids {
|
||||
let mut num_ids: u16 = 0;
|
||||
// `inpos` is right after the open bracket of the array
|
||||
loop {
|
||||
eat_whitespace_and_commas(input, &mut inpos);
|
||||
if input[inpos] == b']' {
|
||||
break;
|
||||
}
|
||||
read_id(input, &mut inpos, &mut output[end..])?;
|
||||
num_ids += 1;
|
||||
end += ID_SIZE;
|
||||
}
|
||||
|
||||
// Write num_ids
|
||||
put(output, NUM_IDS_OFFSET, num_ids.to_ne_bytes().as_slice())?;
|
||||
}
|
||||
|
||||
// Copy authors
|
||||
if let Some(mut inpos) = start_authors {
|
||||
let mut num_authors: u16 = 0;
|
||||
// `inpos` is right after the open bracket of the array
|
||||
loop {
|
||||
eat_whitespace_and_commas(input, &mut inpos);
|
||||
if input[inpos] == b']' {
|
||||
break;
|
||||
}
|
||||
read_pubkey(input, &mut inpos, &mut output[end..])?;
|
||||
num_authors += 1;
|
||||
end += PUBKEY_SIZE;
|
||||
}
|
||||
|
||||
// write num_authors
|
||||
put(
|
||||
output,
|
||||
NUM_AUTHORS_OFFSET,
|
||||
num_authors.to_ne_bytes().as_slice(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Copy kinds
|
||||
if let Some(mut inpos) = start_kinds {
|
||||
let mut num_kinds: u16 = 0;
|
||||
// `inpos` is right after the open bracket of the array
|
||||
loop {
|
||||
eat_whitespace_and_commas(input, &mut inpos);
|
||||
if input[inpos] == b']' {
|
||||
break;
|
||||
}
|
||||
let u = read_u64(input, &mut inpos)?;
|
||||
if u > 65535 {
|
||||
return Err(
|
||||
ChorusError::JsonBadFilter("Filter has kind number too large", inpos).into(),
|
||||
);
|
||||
}
|
||||
put(output, end, (u as u16).to_ne_bytes().as_slice())?;
|
||||
num_kinds += 1;
|
||||
end += KIND_SIZE;
|
||||
}
|
||||
|
||||
// write num_kinds
|
||||
put(output, NUM_KINDS_OFFSET, num_kinds.to_ne_bytes().as_slice())?;
|
||||
}
|
||||
|
||||
// Copy tags
|
||||
{
|
||||
let write_tags_start = end;
|
||||
// write number of tags
|
||||
put(
|
||||
output,
|
||||
write_tags_start + 2,
|
||||
(num_tag_fields as u16).to_ne_bytes().as_slice(),
|
||||
)?;
|
||||
// bump end past offset fields
|
||||
end += 4 + 2 * num_tag_fields;
|
||||
// Now pull in each tag
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for w in 0..num_tag_fields {
|
||||
// Write it's offset
|
||||
put(
|
||||
output,
|
||||
write_tags_start + 4 + (2 * w),
|
||||
((end - write_tags_start) as u16).to_ne_bytes().as_slice(),
|
||||
)?;
|
||||
|
||||
let mut inpos = start_tags[w];
|
||||
let letter = input[inpos];
|
||||
|
||||
// bump past count output and write letter
|
||||
let countindex = end;
|
||||
end += 2;
|
||||
put(output, end, 1_u16.to_ne_bytes().as_slice())?;
|
||||
if output.len() < end + 2 {
|
||||
return Err(crate::error::ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
output[end + 2] = letter;
|
||||
|
||||
// bump past what we just wrote
|
||||
end += 3;
|
||||
|
||||
// scan further in input
|
||||
inpos += 1; // move off letter
|
||||
verify_char(input, b'"', &mut inpos)?;
|
||||
eat_colon_with_whitespace(input, &mut inpos)?;
|
||||
verify_char(input, b'[', &mut inpos)?;
|
||||
|
||||
let mut count: u16 = 1; // the tag letter itself counts
|
||||
loop {
|
||||
eat_whitespace_and_commas(input, &mut inpos);
|
||||
if input[inpos] == b']' {
|
||||
break;
|
||||
}
|
||||
verify_char(input, b'"', &mut inpos)?;
|
||||
// copy data
|
||||
let (inlen, outlen) = json_unescape(&input[inpos..], &mut output[end + 2..])?;
|
||||
// write len
|
||||
put(output, end, (outlen as u16).to_ne_bytes().as_slice())?;
|
||||
end += 2 + outlen;
|
||||
inpos += inlen + 1;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// write count
|
||||
put(output, countindex, count.to_ne_bytes().as_slice())?;
|
||||
}
|
||||
// write length of tags section
|
||||
put(
|
||||
output,
|
||||
write_tags_start,
|
||||
((end - write_tags_start) as u16).to_ne_bytes().as_slice(),
|
||||
)?;
|
||||
}
|
||||
|
||||
if end > u32::MAX as usize {
|
||||
return Err(ChorusError::JsonBadFilter("Filter is too long", end).into());
|
||||
}
|
||||
|
||||
// Write length of filter
|
||||
put(output, 0, (end as u32).to_ne_bytes().as_slice())?;
|
||||
|
||||
Ok((inpos, end))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::types::{Filter, Kind, Pubkey, Tags, TagsIter, TagsStringIter, Time};
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_empty_filter() {
|
||||
let json = br##"{}"##;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer.resize(4096, 0);
|
||||
let (consumed, size) = parse_json_filter(&json[..], &mut buffer).unwrap();
|
||||
assert_eq!(consumed, 2);
|
||||
assert_eq!(size, 36);
|
||||
assert_eq!(
|
||||
&buffer[0..size],
|
||||
&[
|
||||
36, 0, 0, 0, // length
|
||||
0, 0, // 0 ids
|
||||
0, 0, // 0 authors
|
||||
0, 0, // 0 kinds
|
||||
0, 0, // padding
|
||||
255, 255, 255, 255, // max limit
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // since 1970
|
||||
255, 255, 255, 255, 255, 255, 255, 255, // until max unixtime
|
||||
4, 0, 0, 0, // empty tags section
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_filter1() {
|
||||
let json = br##"{"kinds":[1,30023],"since":1681778790,"authors":["e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"],"until":1704238196,"ids" : [ "7089afc2e77f366bc0fd1662e4048f59f18391c04a35957f21bbd1f3e6a492c4"],"limit":10}"##;
|
||||
// ,"#e":"a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7"}"##;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer.resize(4096, 0);
|
||||
let (consumed, size) = parse_json_filter(&json[..], &mut buffer).unwrap();
|
||||
assert_eq!(consumed, json.len());
|
||||
assert_eq!(size, 136);
|
||||
assert_eq!(
|
||||
&buffer[0..size],
|
||||
&[
|
||||
136, 0, 0, 0, // length
|
||||
1, 0, // 0 ids
|
||||
2, 0, // 0 authors
|
||||
2, 0, // 0 kinds
|
||||
0, 0, // padding
|
||||
10, 0, 0, 0, // max limit 10
|
||||
102, 232, 61, 100, 0, 0, 0, 0, // since 1681778790
|
||||
116, 156, 148, 101, 0, 0, 0, 0, // until 1704238196
|
||||
// First ID:
|
||||
112, 137, 175, 194, 231, 127, 54, 107, 192, 253, 22, 98, 228, 4, 143, 89, 241, 131,
|
||||
145, 192, 74, 53, 149, 127, 33, 187, 209, 243, 230, 164, 146, 196,
|
||||
// First author:
|
||||
226, 204, 247, 207, 32, 64, 63, 63, 42, 74, 85, 179, 40, 240, 222, 59, 227, 133, 88,
|
||||
167, 213, 243, 54, 50, 253, 170, 239, 199, 38, 193, 200, 235,
|
||||
// Second author:
|
||||
44, 134, 171, 204, 152, 247, 253, 138, 103, 80, 170, 184, 223, 108, 24, 99, 144, 63,
|
||||
16, 114, 6, 204, 45, 114, 232, 175, 235, 108, 56, 53, 122, 237, // Kinds,
|
||||
1, 0, // 1
|
||||
71, 117, // 30023
|
||||
4, 0, 0, 0, // empty tags section
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_json_filter2() {
|
||||
let json = br##"{"kinds":[1,30023],"since":1681778790,"authors":["e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"],"until":1704238196,"ids" : [ "7089afc2e77f366bc0fd1662e4048f59f18391c04a35957f21bbd1f3e6a492c4"],"limit":10, "#e":["a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7"]}"##;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer.resize(4096, 0);
|
||||
let (consumed, size) = parse_json_filter(&json[..], &mut buffer).unwrap();
|
||||
assert_eq!(consumed, json.len());
|
||||
assert_eq!(size, 209);
|
||||
assert_eq!(
|
||||
&buffer[0..size],
|
||||
&[
|
||||
209, 0, 0, 0, // length
|
||||
1, 0, // 0 ids
|
||||
2, 0, // 0 authors
|
||||
2, 0, // 0 kinds
|
||||
0, 0, // padding
|
||||
10, 0, 0, 0, // max limit 10
|
||||
102, 232, 61, 100, 0, 0, 0, 0, // since 1681778790
|
||||
116, 156, 148, 101, 0, 0, 0, 0, // until 1704238196
|
||||
// First ID:
|
||||
112, 137, 175, 194, 231, 127, 54, 107, 192, 253, 22, 98, 228, 4, 143, 89, 241, 131,
|
||||
145, 192, 74, 53, 149, 127, 33, 187, 209, 243, 230, 164, 146, 196,
|
||||
// First author:
|
||||
226, 204, 247, 207, 32, 64, 63, 63, 42, 74, 85, 179, 40, 240, 222, 59, 227, 133, 88,
|
||||
167, 213, 243, 54, 50, 253, 170, 239, 199, 38, 193, 200, 235,
|
||||
// Second author:
|
||||
44, 134, 171, 204, 152, 247, 253, 138, 103, 80, 170, 184, 223, 108, 24, 99, 144, 63,
|
||||
16, 114, 6, 204, 45, 114, 232, 175, 235, 108, 56, 53, 122, 237, // Kinds,
|
||||
1, 0, // 1
|
||||
71, 117, // 30023
|
||||
// Tag section:
|
||||
77, 0, // tags section length is 77
|
||||
1, 0, // just one tag
|
||||
6, 0, // offset of 0th tag is 6
|
||||
// First tag:
|
||||
2, 0, // 2 fields
|
||||
// Field 1:
|
||||
1, 0, // 1 byte long
|
||||
101, // 'e'
|
||||
// Field 2:
|
||||
64, 0, // 64 bytes long
|
||||
97, 57, 54, 54, 51, 48, 53, 53, 49, 54, 52, 97, 98, 56, 98, 51, 48, 100, 57, 53,
|
||||
50, 52, 54, 53, 54, 51, 55, 48, 99, 52, 98, 102, 57, 51, 51, 57, 51, 98, 98, 48,
|
||||
53, 49, 98, 55, 101, 100, 102, 52, 53, 53, 54, 102, 52, 48, 99, 53, 50, 57, 56,
|
||||
100, 99, 48, 99, 55
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_parse_and_check() {
|
||||
let json = br##"{"kinds":[1,5,9,30023],"since":1681778790,"authors":["e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"], "#e":["a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"],"#p":["2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed","2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"]}"##;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(4096);
|
||||
buffer.resize(4096, 0);
|
||||
let (consumed, size) = parse_json_filter(&json[..], &mut buffer).unwrap();
|
||||
assert_eq!(consumed, json.len());
|
||||
assert_eq!(size, 452);
|
||||
let filter = Filter::delineate(&buffer).unwrap();
|
||||
assert_eq!(filter.len(), 452);
|
||||
assert_eq!(filter.num_ids(), 0);
|
||||
|
||||
assert_eq!(filter.num_authors(), 2);
|
||||
let mut author_iter = filter.authors();
|
||||
assert_eq!(
|
||||
author_iter.next(),
|
||||
Some(
|
||||
Pubkey::read_hex(
|
||||
b"e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
author_iter.next(),
|
||||
Some(
|
||||
Pubkey::read_hex(
|
||||
b"2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(author_iter.next(), None);
|
||||
|
||||
assert_eq!(filter.num_kinds(), 4);
|
||||
let mut kind_iter = filter.kinds();
|
||||
assert_eq!(kind_iter.next(), Some(Kind(1)));
|
||||
assert_eq!(kind_iter.next(), Some(Kind(5)));
|
||||
assert_eq!(kind_iter.next(), Some(Kind(9)));
|
||||
assert_eq!(kind_iter.next(), Some(Kind(30023)));
|
||||
assert_eq!(kind_iter.next(), None);
|
||||
|
||||
assert_eq!(filter.limit(), u32::MAX);
|
||||
assert_eq!(filter.since(), Time(1681778790));
|
||||
assert_eq!(filter.until(), Time::max());
|
||||
|
||||
let tags: Tags = filter.tags().unwrap();
|
||||
let mut tag_iter: TagsIter = tags.iter();
|
||||
let mut tag1_iter: TagsStringIter = tag_iter.next().unwrap();
|
||||
assert_eq!(tag1_iter.next(), Some(b"e".as_slice()));
|
||||
assert_eq!(
|
||||
tag1_iter.next(),
|
||||
Some(b"a9663055164ab8b30d9524656370c4bf93393bb051b7edf4556f40c5298dc0c7".as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
tag1_iter.next(),
|
||||
Some(b"2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed".as_slice())
|
||||
);
|
||||
assert_eq!(tag1_iter.next(), None);
|
||||
let mut tag2_iter = tag_iter.next().unwrap();
|
||||
assert_eq!(tag2_iter.next(), Some(b"p".as_slice()));
|
||||
assert_eq!(
|
||||
tag2_iter.next(),
|
||||
Some(b"2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed".as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
tag2_iter.next(),
|
||||
Some(b"2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed".as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
tag2_iter.next(),
|
||||
Some(b"2c86abcc98f7fd8a6750aab8df6c1863903f107206cc2d72e8afeb6c38357aed".as_slice())
|
||||
);
|
||||
assert_eq!(tag2_iter.next(), None);
|
||||
assert!(tag_iter.next().is_none());
|
||||
}
|
||||
}
|
||||
@ -1,537 +0,0 @@
|
||||
use super::{Event, Id, Kind, Pubkey, Tags, Time};
|
||||
use crate::error::{ChorusError, Error};
|
||||
use std::fmt;
|
||||
|
||||
mod json_filter;
|
||||
use json_filter::parse_json_filter;
|
||||
|
||||
/*
|
||||
* 0 [4 bytes] length of entire structure
|
||||
* 4 [2 bytes] num_ids
|
||||
* 6 [2 bytes] num_authors
|
||||
* 8 [2 bytes] num_kinds
|
||||
* 10 [2 bytes] PADDING
|
||||
* 12 [4 bytes] limit u32. Set to u32::max if limit was not set.
|
||||
* 16 [8 bytes] since u64. Set to 0 if since was not set.
|
||||
* 24 [8 bytes] until u64. Set to u64::max if until was not set.
|
||||
* 32 [ID] array
|
||||
* [Pubkey] array starts at 32 + num_ids*32
|
||||
* [Kind] array starts at 32 + num_ids*32 + num_authors*32
|
||||
* [Tags] object starts at 32 + num_ids*32 + num_authors*32 * num_kinds*2
|
||||
*/
|
||||
|
||||
const NUM_IDS_OFFSET: usize = 4;
|
||||
const NUM_AUTHORS_OFFSET: usize = 6;
|
||||
const NUM_KINDS_OFFSET: usize = 8;
|
||||
const LIMIT_OFFSET: usize = 12;
|
||||
const SINCE_OFFSET: usize = 16;
|
||||
const UNTIL_OFFSET: usize = 24;
|
||||
const ARRAYS_OFFSET: usize = 32;
|
||||
const ID_SIZE: usize = 32;
|
||||
const PUBKEY_SIZE: usize = 32;
|
||||
const KIND_SIZE: usize = 2;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Filter<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> Filter<'a> {
|
||||
// Parse json into a Filter. Returns the count of consumed input bytes and output
|
||||
// bytes the Filter
|
||||
pub fn from_json(
|
||||
json: &[u8],
|
||||
output_buffer: &'a mut [u8],
|
||||
) -> Result<(usize, usize, Filter<'a>), Error> {
|
||||
let (incount, outcount) = parse_json_filter(json, output_buffer)?;
|
||||
Ok((incount, outcount, Filter(&output_buffer[..outcount])))
|
||||
}
|
||||
|
||||
pub fn delineate(input: &'a [u8]) -> Result<Filter<'a>, Error> {
|
||||
if input.len() < ARRAYS_OFFSET {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
let len = parse_u32!(input, 0) as usize;
|
||||
if input.len() < len {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
Ok(Filter(&input[0..len]))
|
||||
}
|
||||
|
||||
pub fn copy(&self, output: &mut [u8]) -> Result<(), Error> {
|
||||
if output.len() < self.0.len() {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
output[..self.0.len()].copy_from_slice(self.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn num_ids(&self) -> usize {
|
||||
parse_u16!(self.0, NUM_IDS_OFFSET) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ids(&'a self) -> FilterIdIter<'a> {
|
||||
FilterIdIter {
|
||||
filter: self,
|
||||
next: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn num_authors(&self) -> usize {
|
||||
parse_u16!(self.0, NUM_AUTHORS_OFFSET) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn start_of_authors(&self) -> usize {
|
||||
ARRAYS_OFFSET + self.num_ids() * ID_SIZE
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn authors(&'a self) -> FilterAuthorIter<'a> {
|
||||
FilterAuthorIter {
|
||||
filter: self,
|
||||
start_of_authors: self.start_of_authors(),
|
||||
next: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn num_kinds(&self) -> usize {
|
||||
parse_u16!(self.0, NUM_KINDS_OFFSET) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn start_of_kinds(&self) -> usize {
|
||||
ARRAYS_OFFSET + self.num_ids() * ID_SIZE + self.num_authors() * PUBKEY_SIZE
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn kinds(&'a self) -> FilterKindIter<'a> {
|
||||
FilterKindIter {
|
||||
filter: self,
|
||||
start_of_kinds: self.start_of_kinds(),
|
||||
next: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn start_of_tags(&self) -> usize {
|
||||
ARRAYS_OFFSET
|
||||
+ self.num_ids() * ID_SIZE
|
||||
+ self.num_authors() * PUBKEY_SIZE
|
||||
+ self.num_kinds() * KIND_SIZE
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn tags(&'a self) -> Result<Tags<'a>, Error> {
|
||||
Tags::delineate(&self.0[self.start_of_tags()..])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn limit(&self) -> u32 {
|
||||
parse_u32!(self.0, LIMIT_OFFSET)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn since(&self) -> Time {
|
||||
Time(parse_u64!(self.0, SINCE_OFFSET))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn until(&self) -> Time {
|
||||
Time(parse_u64!(self.0, UNTIL_OFFSET))
|
||||
}
|
||||
|
||||
pub fn event_matches(&self, event: &Event) -> Result<bool, Error> {
|
||||
// ids
|
||||
if self.num_ids() != 0 && !self.ids().any(|id| id == event.id()) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// authors
|
||||
if self.num_authors() != 0 && !self.authors().any(|pk| pk == event.pubkey()) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// kinds
|
||||
if self.num_kinds() != 0 && !self.kinds().any(|kind| kind == event.kind()) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// since
|
||||
if event.created_at() < self.since() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// until
|
||||
if event.created_at() > self.until() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// tags
|
||||
let filter_tags = self.tags()?;
|
||||
if !filter_tags.is_empty() {
|
||||
let event_tags = event.tags()?;
|
||||
if event_tags.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while let Some(letter) = filter_tags.get_string(i, 0) {
|
||||
let mut j = 1;
|
||||
let mut found = false;
|
||||
while let Some(value) = filter_tags.get_string(i, j) {
|
||||
if event_tags.matches(letter, value) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
if !found {
|
||||
return Ok(false);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn as_json(&self) -> Result<Vec<u8>, Error> {
|
||||
let mut output: Vec<u8> = Vec::with_capacity(256);
|
||||
output.push(b'{');
|
||||
let mut first = true;
|
||||
|
||||
if self.num_ids() > 0 {
|
||||
output.extend(br#""ids":["#);
|
||||
for (i, id) in self.ids().enumerate() {
|
||||
if i > 0 {
|
||||
output.push(b',');
|
||||
}
|
||||
output.push(b'"');
|
||||
let pos = output.len();
|
||||
output.resize(pos + 64, 0);
|
||||
id.write_hex(&mut output[pos..])?;
|
||||
output.push(b'"');
|
||||
}
|
||||
output.push(b']');
|
||||
first = false;
|
||||
}
|
||||
|
||||
if self.num_authors() > 0 {
|
||||
if !first {
|
||||
output.push(b',');
|
||||
}
|
||||
output.extend(br#""authors":["#);
|
||||
for (i, pk) in self.authors().enumerate() {
|
||||
if i > 0 {
|
||||
output.push(b',');
|
||||
}
|
||||
output.push(b'"');
|
||||
let pos = output.len();
|
||||
output.resize(pos + 64, 0);
|
||||
pk.write_hex(&mut output[pos..])?;
|
||||
output.push(b'"');
|
||||
}
|
||||
output.push(b']');
|
||||
first = false;
|
||||
}
|
||||
|
||||
if self.num_kinds() > 0 {
|
||||
if !first {
|
||||
output.push(b',');
|
||||
}
|
||||
output.extend(br#""kinds":["#);
|
||||
for (i, k) in self.kinds().enumerate() {
|
||||
if i > 0 {
|
||||
output.push(b',');
|
||||
}
|
||||
output.extend(format!("{}", k.0).as_bytes());
|
||||
}
|
||||
output.push(b']');
|
||||
first = false;
|
||||
}
|
||||
|
||||
let tags = self.tags()?;
|
||||
if !tags.is_empty() {
|
||||
// Filter 'tags' are not an array of arrays, they are just a convenient
|
||||
// way to store similar data. They also elide the '#'. So we have to
|
||||
// iterate here, we cannot use tags.as_json()
|
||||
for tag in tags.iter() {
|
||||
if !first {
|
||||
output.push(b',');
|
||||
}
|
||||
for (i, bytes) in tag.enumerate() {
|
||||
if i == 0 {
|
||||
output.extend(b"\"#");
|
||||
output.extend(bytes);
|
||||
output.extend(b"\":[");
|
||||
} else {
|
||||
if i > 1 {
|
||||
output.push(b',');
|
||||
}
|
||||
output.push(b'"');
|
||||
output.extend(bytes);
|
||||
output.push(b'"');
|
||||
}
|
||||
}
|
||||
output.push(b']');
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.limit() != u32::MAX {
|
||||
if !first {
|
||||
output.push(b',');
|
||||
}
|
||||
output.extend(format!(r#""limit":{}"#, self.limit()).as_bytes());
|
||||
first = false;
|
||||
}
|
||||
|
||||
if self.since() != Time::min() {
|
||||
if !first {
|
||||
output.push(b',');
|
||||
}
|
||||
output.extend(format!(r#""since":{}"#, self.since().0).as_bytes());
|
||||
first = false;
|
||||
}
|
||||
|
||||
if self.until() != Time::max() {
|
||||
if !first {
|
||||
output.push(b',');
|
||||
}
|
||||
output.extend(format!(r#""until":{}"#, self.until().0).as_bytes());
|
||||
}
|
||||
|
||||
output.push(b'}');
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Filter<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Ok(bytes) = self.as_json() {
|
||||
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
write!(f, "{s}")
|
||||
} else {
|
||||
write!(f, "{{Corrupted Event}}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FilterIdIter<'a> {
|
||||
filter: &'a Filter<'a>,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FilterIdIter<'a> {
|
||||
type Item = Id;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let num_ids = parse_u16!(self.filter.0, NUM_IDS_OFFSET) as usize;
|
||||
if self.next >= num_ids {
|
||||
None
|
||||
} else {
|
||||
let offset = ARRAYS_OFFSET + self.next * ID_SIZE;
|
||||
self.next += 1;
|
||||
if self.filter.0.len() < offset + ID_SIZE {
|
||||
None
|
||||
} else {
|
||||
Some(Id(self.filter.0[offset..offset + ID_SIZE]
|
||||
.try_into()
|
||||
.unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FilterAuthorIter<'a> {
|
||||
filter: &'a Filter<'a>,
|
||||
start_of_authors: usize,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FilterAuthorIter<'a> {
|
||||
type Item = Pubkey;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let num_authors = parse_u16!(self.filter.0, NUM_AUTHORS_OFFSET) as usize;
|
||||
if self.next >= num_authors {
|
||||
None
|
||||
} else {
|
||||
let offset = self.start_of_authors + self.next * PUBKEY_SIZE;
|
||||
self.next += 1;
|
||||
if self.filter.0.len() < offset + PUBKEY_SIZE {
|
||||
None
|
||||
} else {
|
||||
Some(Pubkey(
|
||||
self.filter.0[offset..offset + PUBKEY_SIZE]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FilterKindIter<'a> {
|
||||
filter: &'a Filter<'a>,
|
||||
start_of_kinds: usize,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FilterKindIter<'a> {
|
||||
type Item = Kind;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let num_kinds = parse_u16!(self.filter.0, NUM_KINDS_OFFSET) as usize;
|
||||
if self.next >= num_kinds {
|
||||
None
|
||||
} else {
|
||||
let offset = self.start_of_kinds + self.next * KIND_SIZE;
|
||||
self.next += 1;
|
||||
if self.filter.0.len() < offset + KIND_SIZE {
|
||||
None
|
||||
} else {
|
||||
Some(Kind(parse_u16!(self.filter.0, offset)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OwnedFilter(pub Vec<u8>);
|
||||
|
||||
impl OwnedFilter {
|
||||
pub fn as_filter(&self) -> Result<Filter<'_>, Error> {
|
||||
Filter::delineate(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_filter() {
|
||||
/*
|
||||
* {
|
||||
* "ids": [ "6b43bc2e373b6d9330ff571f3f4e6d897b32d01d65227df3fa41cdf731c63c3a",
|
||||
* "1f47034c9d6d0539382a86ba31766f00f2b8312ab167c036729422ec9e7085e8"],
|
||||
* "authors": [ "52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd",
|
||||
* "ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49" ],
|
||||
* "kinds": [ 1, 5, 30023 ],
|
||||
* "since": 1702161345,
|
||||
* "#p": [ "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" ],
|
||||
* }
|
||||
*/
|
||||
|
||||
// For comparison
|
||||
let id1 = Id::read_hex(b"6b43bc2e373b6d9330ff571f3f4e6d897b32d01d65227df3fa41cdf731c63c3a")
|
||||
.unwrap();
|
||||
|
||||
let id2 = Id::read_hex(b"1f47034c9d6d0539382a86ba31766f00f2b8312ab167c036729422ec9e7085e8")
|
||||
.unwrap();
|
||||
let pk1 =
|
||||
Pubkey::read_hex(b"52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd")
|
||||
.unwrap();
|
||||
let pk2 =
|
||||
Pubkey::read_hex(b"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49")
|
||||
.unwrap();
|
||||
let tagged =
|
||||
Pubkey::read_hex(b"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52")
|
||||
.unwrap();
|
||||
|
||||
let data: Vec<u8> = vec![
|
||||
211, 0, 0, 0, // length of structure
|
||||
2, 0, // number of IDs
|
||||
2, 0, // number of authors
|
||||
3, 0, // number of kinds
|
||||
0, 0, // padding
|
||||
255, 255, 255, 255, // limit
|
||||
0xC1, 0xEB, 0x74, 0x65, 0, 0, 0, 0, // since
|
||||
255, 255, 255, 255, 255, 255, 255, 255, // until
|
||||
0x6b, 0x43, 0xbc, 0x2e, 0x37, 0x3b, 0x6d, 0x93, 0x30, 0xff, 0x57, 0x1f, 0x3f, 0x4e,
|
||||
0x6d, 0x89, 0x7b, 0x32, 0xd0, 0x1d, 0x65, 0x22, 0x7d, 0xf3, 0xfa, 0x41, 0xcd, 0xf7,
|
||||
0x31, 0xc6, 0x3c, 0x3a, // ID 1
|
||||
0x1f, 0x47, 0x03, 0x4c, 0x9d, 0x6d, 0x05, 0x39, 0x38, 0x2a, 0x86, 0xba, 0x31, 0x76,
|
||||
0x6f, 0x00, 0xf2, 0xb8, 0x31, 0x2a, 0xb1, 0x67, 0xc0, 0x36, 0x72, 0x94, 0x22, 0xec,
|
||||
0x9e, 0x70, 0x85, 0xe8, // ID 2
|
||||
0x52, 0xb4, 0xa0, 0x76, 0xbc, 0xbb, 0xbd, 0xc3, 0xa1, 0xae, 0xfa, 0x37, 0x35, 0x81,
|
||||
0x6c, 0xf7, 0x49, 0x93, 0xb1, 0xb8, 0xdb, 0x20, 0x2b, 0x01, 0xc8, 0x83, 0xc5, 0x8b,
|
||||
0xe7, 0xfa, 0xd8, 0xbd, // Pubkey 1
|
||||
0xee, 0x11, 0xa5, 0xdf, 0xf4, 0x0c, 0x19, 0xa5, 0x55, 0xf4, 0x1f, 0xe4, 0x2b, 0x48,
|
||||
0xf0, 0x0e, 0x61, 0x8c, 0x91, 0x22, 0x56, 0x22, 0xae, 0x37, 0xb6, 0xc2, 0xbb, 0x67,
|
||||
0xb7, 0x6c, 0x4e, 0x49, // Pubkey 2
|
||||
1, 0, 5, 0, 71, 117, // 3 kinds
|
||||
// Tags
|
||||
45, 0, // tags_len
|
||||
1, 0, // num_tags
|
||||
6, 0, // first tag offset at 6
|
||||
2, 0, // 2 fields long
|
||||
1, 0, // 1st field is 1 byte
|
||||
112, // "p"
|
||||
32, 0, // 2nd field is 32 bytes
|
||||
// 2nd field
|
||||
0xfa, 0x98, 0x4b, 0xd7, 0xdb, 0xb2, 0x82, 0xf0, 0x7e, 0x16, 0xe7, 0xae, 0x87, 0xb2,
|
||||
0x6a, 0x2a, 0x7b, 0x9b, 0x90, 0xb7, 0x24, 0x6a, 0x44, 0x77, 0x1f, 0x0c, 0xf5, 0xae,
|
||||
0x58, 0x01, 0x8f, 0x52,
|
||||
];
|
||||
|
||||
let filter = Filter::delineate(&data).unwrap();
|
||||
|
||||
assert_eq!(filter.num_ids(), 2);
|
||||
let mut ids = filter.ids();
|
||||
assert_eq!(ids.next().unwrap(), id1);
|
||||
assert_eq!(ids.next().unwrap(), id2);
|
||||
assert!(ids.next().is_none());
|
||||
|
||||
assert_eq!(filter.num_authors(), 2);
|
||||
let mut authors = filter.authors();
|
||||
assert_eq!(authors.next().unwrap(), pk1);
|
||||
assert_eq!(authors.next().unwrap(), pk2);
|
||||
assert!(authors.next().is_none());
|
||||
|
||||
assert_eq!(filter.num_kinds(), 3);
|
||||
let mut kinds = filter.kinds();
|
||||
assert_eq!(kinds.next().unwrap(), Kind(1));
|
||||
assert_eq!(kinds.next().unwrap(), Kind(5));
|
||||
assert_eq!(kinds.next().unwrap(), Kind(30023));
|
||||
assert!(kinds.next().is_none());
|
||||
|
||||
assert_eq!(filter.limit(), u32::MAX);
|
||||
assert_eq!(filter.since(), Time(1702161345));
|
||||
assert_eq!(filter.until(), Time::max());
|
||||
|
||||
let tags = filter.tags().unwrap();
|
||||
assert_eq!(tags.len(), 1);
|
||||
let mut iter = tags.iter();
|
||||
let mut tag = iter.next().unwrap();
|
||||
assert!(iter.next().is_none());
|
||||
assert_eq!(tag.next().unwrap(), b"p");
|
||||
let p_bytes = tag.next().unwrap();
|
||||
assert!(tag.next().is_none());
|
||||
let pk = Pubkey(p_bytes.try_into().unwrap());
|
||||
assert_eq!(pk, tagged);
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
use crate::error::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Id(pub [u8; 32]);
|
||||
|
||||
impl Id {
|
||||
pub fn write_hex(&self, output: &mut [u8]) -> Result<(), Error> {
|
||||
write_hex!(self.0, output, 32)
|
||||
}
|
||||
|
||||
pub fn read_hex(input: &[u8]) -> Result<Id, Error> {
|
||||
let mut out: [u8; 32] = [0; 32];
|
||||
read_hex!(input, &mut out, 32)?;
|
||||
Ok(Id(out))
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
self.0.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Id {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut bytes: [u8; 64] = [0; 64];
|
||||
self.write_hex(&mut bytes).unwrap();
|
||||
let hex = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
write!(f, "{hex}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Id;
|
||||
|
||||
#[test]
|
||||
fn test_id_hex_functions() {
|
||||
let hex = b"1110ee4ff957fa9c55832eaccb4dc1c45bfc6304e1e4e9fa478f53df4b20062d";
|
||||
let id = Id::read_hex(hex).unwrap();
|
||||
eprintln!("{:?}", id);
|
||||
let mut hex2: [u8; 64] = [0; 64];
|
||||
id.write_hex(&mut hex2).unwrap();
|
||||
assert_eq!(hex, &hex2);
|
||||
assert_eq!(format!("{}", id).as_bytes(), hex);
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Kind(pub u16);
|
||||
|
||||
impl fmt::Display for Kind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
// Some kinds a relay may need to treat differently
|
||||
pub const SEAL: u16 = 13;
|
||||
pub const DM_CHAT: u16 = 14;
|
||||
|
||||
pub fn is_replaceable(&self) -> bool {
|
||||
(10000..20000).contains(&self.0) || self.0 == 0 || self.0 == 3
|
||||
}
|
||||
|
||||
pub fn is_ephemeral(&self) -> bool {
|
||||
(20000..30000).contains(&self.0)
|
||||
}
|
||||
|
||||
pub fn is_parameterized_replaceable(&self) -> bool {
|
||||
(30000..40000).contains(&self.0)
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
mod event;
|
||||
pub use event::{Event, OwnedEvent};
|
||||
|
||||
mod filter;
|
||||
pub use filter::{Filter, OwnedFilter};
|
||||
|
||||
mod id;
|
||||
pub use id::Id;
|
||||
|
||||
mod kind;
|
||||
pub use kind::Kind;
|
||||
|
||||
pub mod parse;
|
||||
|
||||
mod pubkey;
|
||||
pub use pubkey::Pubkey;
|
||||
|
||||
mod sig;
|
||||
pub use sig::Sig;
|
||||
|
||||
mod tags;
|
||||
pub use tags::{Tags, TagsIter, TagsStringIter};
|
||||
|
||||
mod time;
|
||||
pub use time::Time;
|
||||
@ -1,254 +0,0 @@
|
||||
use super::utf8::{encode_utf8, next_code_point};
|
||||
use crate::error::{ChorusError, Error};
|
||||
|
||||
// LITERAL UNESCAPED: 0x20-0x21, 0x23-0x5B, 0x5D-10FFFF
|
||||
// ESCAPES: \" \\ \/ /b /f /n /r /t
|
||||
// UTF ESCAPE: \uXXXX or \uXXXX\uXXXX
|
||||
|
||||
// This escapes a string with JSON escapes. It takes an output buffer,
|
||||
// and returns it filled with the escaped string.
|
||||
// For performance you should try to pass in a buffer that is already
|
||||
// allocated big enough.
|
||||
pub fn json_escape(input: &[u8], mut out: Vec<u8>) -> Result<Vec<u8>, Error> {
|
||||
let mut read_pos: usize = 0;
|
||||
while let Some((codepoint, size)) = next_code_point(&input[read_pos..])? {
|
||||
if is_safe_char(codepoint) {
|
||||
out.extend(&input[read_pos..read_pos + size]);
|
||||
} else {
|
||||
match codepoint {
|
||||
0x08 => out.extend("\\b".as_bytes()),
|
||||
0x09 => out.extend("\\t".as_bytes()),
|
||||
0x0A => out.extend("\\n".as_bytes()),
|
||||
0x0C => out.extend("\\f".as_bytes()),
|
||||
0x0D => out.extend("\\r".as_bytes()),
|
||||
0x22 => out.extend("\\\"".as_bytes()),
|
||||
0x5C => out.extend("\\\\".as_bytes()),
|
||||
_ => {
|
||||
if codepoint > 0x20 {
|
||||
panic!("unnecessary encoding requested");
|
||||
}
|
||||
// This violates NIP-01 which doesn't allow characters like 0x00
|
||||
// even though JSON UTF-8 does.
|
||||
out.extend(format!("\\u{:04x}", codepoint).as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
read_pos += size;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
macro_rules! output_slice {
|
||||
($slice:expr, $out:expr, $pos:expr) => {
|
||||
if $out.len() < *$pos + $slice.len() {
|
||||
Err(Into::<crate::error::Error>::into(
|
||||
crate::error::ChorusError::BufferTooSmall,
|
||||
))
|
||||
} else {
|
||||
$out[*$pos..*$pos + $slice.len()].copy_from_slice($slice);
|
||||
*$pos += $slice.len();
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! output_byte {
|
||||
($byte:expr, $out:expr, $pos:expr) => {
|
||||
if $out.len() < *$pos + 1 {
|
||||
Err(Into::<crate::error::Error>::into(
|
||||
crate::error::ChorusError::BufferTooSmall,
|
||||
))
|
||||
} else {
|
||||
unsafe { *$out.get_unchecked_mut(*$pos) = $byte };
|
||||
*$pos += 1;
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// This unescapes a JSON string into the output.
|
||||
///
|
||||
/// The input should start on the first character of the string, and may extend
|
||||
/// to the ending double-quote and even further.
|
||||
///
|
||||
/// This will return how much input was consumed and how much output was written
|
||||
/// in that order (input_len, output_len)
|
||||
pub fn json_unescape(input: &[u8], out: &mut [u8]) -> Result<(usize, usize), Error> {
|
||||
const BACKSPACE: u8 = 0x08;
|
||||
const FORMFEED: u8 = 0x0C;
|
||||
const LINEFEED: u8 = 0x0A;
|
||||
const CR: u8 = 0x0D;
|
||||
const TAB: u8 = 0x09;
|
||||
const QUOTE: u8 = 0x22;
|
||||
const BACKSLASH: u8 = 0x5C;
|
||||
const SLASH: u8 = 0x2F;
|
||||
|
||||
// Write position in the output buffer
|
||||
let mut write_pos: usize = 0;
|
||||
|
||||
let mut inescape: bool = false;
|
||||
let mut uescape: Option<(usize, u32)> = None;
|
||||
let mut p: usize = 0;
|
||||
while let Some((codepoint, size)) = next_code_point(&input[p..])? {
|
||||
if inescape {
|
||||
inescape = false;
|
||||
if codepoint > 255 {
|
||||
return Err(ChorusError::JsonEscape.into());
|
||||
}
|
||||
match codepoint as u8 {
|
||||
QUOTE | BACKSLASH | SLASH => {
|
||||
output_slice!(&input[p..p + size], out, &mut write_pos)?
|
||||
}
|
||||
b'b' => output_byte!(BACKSPACE, out, &mut write_pos)?,
|
||||
b'f' => output_byte!(FORMFEED, out, &mut write_pos)?,
|
||||
b'n' => output_byte!(LINEFEED, out, &mut write_pos)?,
|
||||
b'r' => output_byte!(CR, out, &mut write_pos)?,
|
||||
b't' => output_byte!(TAB, out, &mut write_pos)?,
|
||||
// This violates NIP-01 but we accept it
|
||||
b'u' => uescape = Some((0, 0)),
|
||||
_ => return Err(ChorusError::JsonEscape.into()), // nothing else is a legal escape
|
||||
}
|
||||
} else if let Some((digit, total)) = uescape {
|
||||
// must be a digit
|
||||
if !(48..=57).contains(&codepoint) {
|
||||
return Err(ChorusError::JsonEscape.into());
|
||||
}
|
||||
let total = total + ((codepoint - 48) << (4 * (3 - digit)));
|
||||
if digit >= 3 {
|
||||
if (0xD800..=0xDFFF).contains(&total) {
|
||||
return Err(ChorusError::JsonEscapeSurrogate.into());
|
||||
}
|
||||
let s = encode_utf8(total, &mut out[write_pos..])?;
|
||||
write_pos += s;
|
||||
uescape = None;
|
||||
} else {
|
||||
uescape = Some((digit + 1, total));
|
||||
}
|
||||
} else if codepoint == 0x5C {
|
||||
// backslash
|
||||
inescape = true;
|
||||
} else if is_safe_char(codepoint) {
|
||||
output_slice!(&input[p..p + size], out, &mut write_pos)?;
|
||||
} else if codepoint == 0x22 {
|
||||
// ending double quote
|
||||
break;
|
||||
} else {
|
||||
return Err(ChorusError::JsonBadStringChar(codepoint).into());
|
||||
}
|
||||
p += size;
|
||||
}
|
||||
|
||||
Ok((p, write_pos))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_safe_char(c: u32) -> bool {
|
||||
let safe_ranges = [(0x20..=0x21), (0x23..=0x5B), (0x5D..=0x10FFFF)];
|
||||
safe_ranges.iter().any(|range| range.contains(&c))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{json_escape, json_unescape};
|
||||
|
||||
#[test]
|
||||
fn test_json_escape() {
|
||||
let buffer = Vec::with_capacity(255);
|
||||
|
||||
let input = "hello\t\tworld
|
||||
!!!";
|
||||
let mut buffer = json_escape(input.as_bytes(), buffer).unwrap();
|
||||
assert_eq!(&buffer[0..19], br#"hello\t\tworld\n!!!"#);
|
||||
|
||||
buffer.clear();
|
||||
|
||||
let input: [u8; 11] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
let mut buffer = json_escape(input.as_slice(), buffer).unwrap();
|
||||
assert_eq!(
|
||||
&buffer[0..54],
|
||||
br#"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n"#
|
||||
);
|
||||
|
||||
buffer.clear();
|
||||
|
||||
let input: [u8; 12] = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22];
|
||||
let mut buffer = json_escape(input.as_slice(), buffer).unwrap();
|
||||
assert_eq!(
|
||||
&buffer[0..64],
|
||||
br#"\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016"#
|
||||
);
|
||||
|
||||
buffer.clear();
|
||||
|
||||
let input: [u8; 4] = [32, 33, 34, 35];
|
||||
let mut buffer = json_escape(input.as_slice(), buffer).unwrap();
|
||||
assert_eq!(&buffer[0..5], br##" !\"#"##);
|
||||
|
||||
buffer.clear();
|
||||
|
||||
let input: [u8; 1] = [92];
|
||||
let buffer = json_escape(input.as_slice(), buffer).unwrap();
|
||||
assert_eq!(&buffer[0..2], br#"\\"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_unescape() {
|
||||
let is_ok = |s: &[u8], equals: &[u8]| {
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(1024);
|
||||
buffer.resize(1024, 0);
|
||||
let r = json_unescape(s, &mut buffer);
|
||||
assert!(r.is_ok());
|
||||
let (_inlen, outlen) = r.unwrap();
|
||||
assert_eq!(outlen, equals.len());
|
||||
assert_eq!(&buffer[0..equals.len()], equals);
|
||||
};
|
||||
|
||||
let is_err = |s: &[u8]| {
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(1024);
|
||||
buffer.resize(1024, 0);
|
||||
let r = json_unescape(s, &mut buffer);
|
||||
assert!(r.is_err());
|
||||
};
|
||||
|
||||
// simple string
|
||||
is_ok(&b"abc".as_slice(), b"abc");
|
||||
|
||||
// carraige return
|
||||
is_ok(&br#"ab\nc"#.as_slice(), b"ab\nc");
|
||||
|
||||
// escaping a character that is not allowed
|
||||
is_err(&br#"ab\zc"#.as_slice());
|
||||
|
||||
// escaping quotes is allowed
|
||||
is_ok(&br#" \"abc\" "#.as_slice(), br#" "abc" "#);
|
||||
|
||||
// high character
|
||||
is_ok(r#"𝄞"#.as_bytes(), "𝄞".as_bytes());
|
||||
|
||||
// high character is interpreted as these four bytes
|
||||
is_ok(r#"𝄞"#.as_bytes(), b"\xF0\x9D\x84\x9E");
|
||||
|
||||
// esacaping a character that is not allowed
|
||||
is_err(r#"\𝄞"#.as_bytes());
|
||||
|
||||
// actual unescaped tab is disallowed
|
||||
is_err("\t".as_bytes());
|
||||
|
||||
// unicode escape and more
|
||||
is_ok(
|
||||
r#"{\"name\":\"BagMan\",\"about\":\"Father.\nHusband.\nNerd: \u2020.\"}"#.as_bytes(),
|
||||
"{\"name\":\"BagMan\",\"about\":\"Father.\nHusband.\nNerd: †.\"}".as_bytes(),
|
||||
);
|
||||
|
||||
// bad unicode escape
|
||||
is_err(r#"\u8f00"#.as_bytes());
|
||||
|
||||
// Check output values
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(1024);
|
||||
buffer.resize(1024, 0);
|
||||
let (inlen, outlen) = json_unescape(br#"the\nclient", "gossip""#, &mut buffer).unwrap();
|
||||
assert_eq!(inlen, 11);
|
||||
assert_eq!(outlen, 10);
|
||||
}
|
||||
}
|
||||
@ -1,476 +0,0 @@
|
||||
use super::json_escape::json_unescape;
|
||||
use super::put;
|
||||
use crate::error::{ChorusError, Error};
|
||||
|
||||
#[inline]
|
||||
pub fn eat_whitespace(input: &[u8], inposp: &mut usize) {
|
||||
while *inposp < input.len() && [0x20, 0x09, 0x0A, 0x0D].contains(&input[*inposp]) {
|
||||
*inposp += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn eat_whitespace_and_commas(input: &[u8], inposp: &mut usize) {
|
||||
while *inposp < input.len() && [0x20, 0x09, 0x0A, 0x0D, b','].contains(&input[*inposp]) {
|
||||
*inposp += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn verify_char(input: &[u8], ch: u8, inposp: &mut usize) -> Result<(), Error> {
|
||||
if *inposp >= input.len() {
|
||||
Err(ChorusError::JsonBad("Too Short or Missing Fields", *inposp).into())
|
||||
} else if input[*inposp] == ch {
|
||||
*inposp += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChorusError::JsonBadCharacter(input[*inposp] as char, *inposp, ch as char).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eat_colon_with_whitespace(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
eat_whitespace(input, inposp);
|
||||
verify_char(input, b':', inposp)?;
|
||||
eat_whitespace(input, inposp);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_object_field(input: &[u8], inposp: &mut usize) -> Result<bool, Error> {
|
||||
eat_whitespace(input, inposp);
|
||||
// next comes either comma or end brace
|
||||
if *inposp >= input.len() {
|
||||
return Err(ChorusError::JsonBad("Too short", *inposp).into());
|
||||
}
|
||||
if input[*inposp] == b'}' {
|
||||
*inposp += 1;
|
||||
Ok(true)
|
||||
} else if input[*inposp] == b',' {
|
||||
*inposp += 1;
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(ChorusError::JsonBad("Unexpected char", *inposp).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_id(input: &[u8], inposp: &mut usize, output: &mut [u8]) -> Result<(), Error> {
|
||||
if output.len() < 32 {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
verify_char(input, b'"', inposp)?;
|
||||
if *inposp + 64 >= input.len() {
|
||||
return Err(ChorusError::JsonBad("Too short reading id", *inposp).into());
|
||||
}
|
||||
// Read the hex ID and write the binary ID into the output event structure
|
||||
read_hex!(&input[*inposp..*inposp + 64], &mut output[..32], 32)?;
|
||||
*inposp += 64;
|
||||
verify_char(input, b'"', inposp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_pubkey(input: &[u8], inposp: &mut usize, output: &mut [u8]) -> Result<(), Error> {
|
||||
if output.len() < 32 {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
verify_char(input, b'"', inposp)?;
|
||||
if *inposp + 64 >= input.len() {
|
||||
return Err(ChorusError::JsonBad("Too short reading pubkey", *inposp).into());
|
||||
}
|
||||
// Read the hex pubkey and write the binary pubkey into the output event structure
|
||||
read_hex!(&input[*inposp..*inposp + 64], &mut output[..32], 32)?;
|
||||
*inposp += 64;
|
||||
verify_char(input, b'"', inposp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_u64(input: &[u8], inposp: &mut usize) -> Result<u64, Error> {
|
||||
let mut value: u64 = 0;
|
||||
let mut any: bool = false;
|
||||
while *inposp < input.len() && b"0123456789".contains(&input[*inposp]) {
|
||||
any = true;
|
||||
value = (value * 10) + (input[*inposp] - 48) as u64;
|
||||
*inposp += 1;
|
||||
}
|
||||
if !any {
|
||||
return Err(ChorusError::JsonBad("Expected a positive integer", *inposp).into());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn read_kind(input: &[u8], inposp: &mut usize) -> Result<u16, Error> {
|
||||
let mut value: u32 = 0;
|
||||
let mut any: bool = false;
|
||||
while *inposp < input.len() && b"0123456789".contains(&input[*inposp]) {
|
||||
any = true;
|
||||
value = (value * 10) + (input[*inposp] - 48) as u32;
|
||||
*inposp += 1;
|
||||
}
|
||||
if !any {
|
||||
return Err(ChorusError::JsonBad(
|
||||
"Kind at must be a positive or zero valued number",
|
||||
*inposp,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if value > 65535 {
|
||||
Err(ChorusError::JsonBad("Kind larger than 65535", *inposp).into())
|
||||
} else {
|
||||
Ok(value as u16)
|
||||
}
|
||||
}
|
||||
|
||||
// From the outer bracket through to the character after the close outer bracket
|
||||
// returns the size of data written to the output
|
||||
pub fn read_tags_array(
|
||||
input: &[u8],
|
||||
inposp: &mut usize,
|
||||
output: &mut [u8],
|
||||
) -> Result<usize, Error> {
|
||||
verify_char(input, b'[', inposp)?; // outer array open brace
|
||||
eat_whitespace(input, inposp);
|
||||
|
||||
if output.len() < 4 {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
|
||||
// NOTE: we cannot write any tag strings until after we have counted the tags.
|
||||
// (our tags structure is optimized for reading, not writing)
|
||||
let num_tags: usize = count_tags(input, *inposp)?;
|
||||
put(output, 2, (num_tags as u16).to_ne_bytes().as_slice())?;
|
||||
|
||||
// Case where we have no tags
|
||||
if num_tags == 0 {
|
||||
put(output, 0, 4_u16.to_ne_bytes().as_slice())?;
|
||||
burn_array(input, inposp)?;
|
||||
return Ok(4);
|
||||
}
|
||||
|
||||
verify_char(input, b'[', inposp)?; // opening brace of first tag
|
||||
eat_whitespace(input, inposp);
|
||||
|
||||
let mut tag_num = 0;
|
||||
let mut outpos: usize = 4 + num_tags * 2;
|
||||
if output.len() < outpos {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
|
||||
loop {
|
||||
// Write the offset of this tag
|
||||
let offset_slot = 4 + tag_num * 2;
|
||||
put(
|
||||
output,
|
||||
offset_slot,
|
||||
(outpos as u16).to_ne_bytes().as_slice(),
|
||||
)?;
|
||||
|
||||
// Read the tag (bumps inpos and outpos)
|
||||
read_tag(input, inposp, output, &mut outpos)?;
|
||||
eat_whitespace(input, inposp);
|
||||
|
||||
// Check what is next
|
||||
match input[*inposp] {
|
||||
b']' => {
|
||||
*inposp += 1;
|
||||
if tag_num != num_tags - 1 {
|
||||
panic!("Tag count mismatch");
|
||||
}
|
||||
break;
|
||||
}
|
||||
b',' => {
|
||||
*inposp += 1;
|
||||
eat_whitespace(input, inposp);
|
||||
verify_char(input, b'[', inposp)?;
|
||||
tag_num += 1;
|
||||
if tag_num >= num_tags {
|
||||
panic!("Tag count mismatch");
|
||||
}
|
||||
eat_whitespace(input, inposp);
|
||||
}
|
||||
_ => return Err(ChorusError::JsonBad("Tag array bad character", *inposp).into()),
|
||||
}
|
||||
}
|
||||
|
||||
// Write length of tags section
|
||||
put(output, 0, (outpos as u16).to_ne_bytes().as_slice())?;
|
||||
|
||||
Ok(outpos)
|
||||
}
|
||||
|
||||
// From the first inner tag bracket, ending after the outer bracket.
|
||||
// This just counts the tags, it does not write output or modify the inpos
|
||||
// This does a quicker pass over the content than actual tag parsing does.
|
||||
pub fn count_tags(input: &[u8], mut inpos: usize) -> Result<usize, Error> {
|
||||
// First non-whitespace character after the opening brace
|
||||
match input[inpos] {
|
||||
b']' => return Ok(0), // no tags
|
||||
b'[' => (), // expected
|
||||
_ => return Err(ChorusError::JsonBad("Tag array bad initial character", inpos).into()),
|
||||
}
|
||||
|
||||
let mut count = 1;
|
||||
inpos += 1;
|
||||
burn_tag(input, &mut inpos)?;
|
||||
eat_whitespace(input, &mut inpos);
|
||||
|
||||
loop {
|
||||
match input[inpos] {
|
||||
b']' => return Ok(count),
|
||||
b',' => {
|
||||
inpos += 1;
|
||||
eat_whitespace(input, &mut inpos);
|
||||
verify_char(input, b'[', &mut inpos)?;
|
||||
count += 1;
|
||||
burn_tag(input, &mut inpos)?;
|
||||
eat_whitespace(input, &mut inpos);
|
||||
}
|
||||
_ => return Err(ChorusError::JsonBad("Tag array bad character", inpos).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_tag(
|
||||
input: &[u8],
|
||||
inposp: &mut usize,
|
||||
output: &mut [u8],
|
||||
outposp: &mut usize,
|
||||
) -> Result<(), Error> {
|
||||
let countpos = *outposp;
|
||||
*outposp += 2;
|
||||
|
||||
// handle empty tag
|
||||
if input[*inposp] == b']' {
|
||||
*inposp += 1;
|
||||
put(output, countpos, 0_u16.to_ne_bytes().as_slice())?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
verify_char(input, b'"', inposp)?;
|
||||
|
||||
let mut num_strings: usize = 1;
|
||||
loop {
|
||||
// read string
|
||||
let (inlen, outlen) = json_unescape(&input[*inposp..], &mut output[*outposp + 2..])?;
|
||||
// write the length before it
|
||||
put(output, *outposp, (outlen as u16).to_ne_bytes().as_slice())?;
|
||||
// bump the outposp past it
|
||||
*outposp += 2 + outlen;
|
||||
// bump the inpos past the string (and the ending quote which isn't counted in the len)
|
||||
*inposp += inlen + 1;
|
||||
|
||||
eat_whitespace(input, inposp);
|
||||
match input[*inposp] {
|
||||
b',' => {
|
||||
*inposp += 1;
|
||||
eat_whitespace(input, inposp);
|
||||
verify_char(input, b'"', inposp)?;
|
||||
num_strings += 1;
|
||||
continue;
|
||||
}
|
||||
b']' => {
|
||||
*inposp += 1;
|
||||
break;
|
||||
}
|
||||
_ => return Err(ChorusError::JsonBad("Tag array bad character", *inposp).into()),
|
||||
}
|
||||
}
|
||||
|
||||
// Write the count of strings at the very start
|
||||
put(
|
||||
output,
|
||||
countpos,
|
||||
(num_strings as u16).to_ne_bytes().as_slice(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_content(
|
||||
input: &[u8],
|
||||
inposp: &mut usize,
|
||||
output: &mut [u8],
|
||||
after_tags: usize,
|
||||
) -> Result<(), Error> {
|
||||
verify_char(input, b'"', inposp)?;
|
||||
|
||||
// Place content 4 bytes beyond tags, to reserve space for content length
|
||||
let (inlen, outlen) = json_unescape(&input[*inposp..], &mut output[after_tags + 4..])?;
|
||||
*inposp += inlen + 1; // +1 to pass the end quote
|
||||
|
||||
// Write content length
|
||||
put(output, after_tags, (outlen as u32).to_ne_bytes().as_slice())?;
|
||||
|
||||
// Write event size
|
||||
let event_len = after_tags + 4 + outlen;
|
||||
put(output, 0, (event_len as u32).to_ne_bytes().as_slice())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME this is too event-offset specific
|
||||
pub fn read_sig(input: &[u8], inposp: &mut usize, output: &mut [u8]) -> Result<(), Error> {
|
||||
if output.len() < 144 {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
verify_char(input, b'"', inposp)?;
|
||||
if *inposp + 128 >= input.len() {
|
||||
return Err(ChorusError::JsonBad("Too short reading sig", *inposp).into());
|
||||
}
|
||||
// Read the hex sig and write the binary sig into the output event structure
|
||||
read_hex!(&input[*inposp..*inposp + 128], &mut output[80..144], 64)?;
|
||||
*inposp += 128;
|
||||
verify_char(input, b'"', inposp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// from the character after the start quote
|
||||
// ending on the character following the end quote
|
||||
pub fn burn_string(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
while *inposp < input.len() && input[*inposp] != b'"' {
|
||||
if input[*inposp] == b'\\' && *inposp + 1 < input.len() {
|
||||
*inposp += 2;
|
||||
} else {
|
||||
*inposp += 1;
|
||||
}
|
||||
}
|
||||
if input[*inposp] == b'"' {
|
||||
*inposp += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChorusError::JsonBad("Unterminated string", *inposp).into())
|
||||
}
|
||||
}
|
||||
|
||||
// from the character after the open brace
|
||||
// ending on the character following the close brace
|
||||
pub fn burn_tag(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
eat_whitespace(input, inposp);
|
||||
// handle empty tag
|
||||
if input[*inposp] == b']' {
|
||||
*inposp += 1;
|
||||
return Ok(());
|
||||
}
|
||||
verify_char(input, b'"', inposp)?;
|
||||
burn_string(input, inposp)?;
|
||||
eat_whitespace(input, inposp);
|
||||
while input[*inposp] == b',' {
|
||||
*inposp += 1;
|
||||
eat_whitespace(input, inposp);
|
||||
verify_char(input, b'"', inposp)?;
|
||||
burn_string(input, inposp)?;
|
||||
eat_whitespace(input, inposp);
|
||||
}
|
||||
verify_char(input, b']', inposp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn burn_key_and_value(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
verify_char(input, b'"', inposp)?;
|
||||
burn_string(input, inposp)?;
|
||||
eat_colon_with_whitespace(input, inposp)?;
|
||||
burn_value(input, inposp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// from the character after the open brace
|
||||
// ending on the character following the close brace
|
||||
pub fn burn_object(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
loop {
|
||||
eat_whitespace_and_commas(input, inposp);
|
||||
|
||||
// Check for the end
|
||||
if input[*inposp] == b'}' {
|
||||
*inposp += 1;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
burn_key_and_value(input, inposp)?;
|
||||
}
|
||||
}
|
||||
|
||||
// from the character after the open bracket
|
||||
// ending on the character following the close bracket
|
||||
pub fn burn_array(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
loop {
|
||||
eat_whitespace_and_commas(input, inposp);
|
||||
|
||||
// Check for the end
|
||||
if input[*inposp] == b']' {
|
||||
*inposp += 1;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
burn_value(input, inposp)?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn burn_value(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
if *inposp >= input.len() {
|
||||
return Err(ChorusError::JsonBad("Too short burning an unused JSON value", *inposp).into());
|
||||
}
|
||||
match input[*inposp] {
|
||||
b'"' => {
|
||||
*inposp += 1;
|
||||
burn_string(input, inposp)?
|
||||
}
|
||||
b'[' => {
|
||||
*inposp += 1;
|
||||
burn_array(input, inposp)?
|
||||
}
|
||||
b'{' => {
|
||||
*inposp += 1;
|
||||
burn_object(input, inposp)?
|
||||
}
|
||||
b't' => burn_true(input, inposp)?,
|
||||
b'f' => burn_false(input, inposp)?,
|
||||
b'n' => burn_null(input, inposp)?,
|
||||
b'-' => burn_number(input, inposp)?,
|
||||
_ => {
|
||||
if b"123456789".contains(&input[*inposp]) {
|
||||
burn_number(input, inposp)?
|
||||
} else {
|
||||
return Err(ChorusError::JsonBad(
|
||||
"Too short burning an unused JSON value",
|
||||
*inposp,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn burn_null(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
if *inposp + 4 <= input.len() && &input[*inposp..*inposp + 4] == b"null" {
|
||||
*inposp += 4;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChorusError::JsonBad("Expected null", *inposp).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn burn_true(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
if *inposp + 4 <= input.len() && &input[*inposp..*inposp + 4] == b"true" {
|
||||
*inposp += 4;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChorusError::JsonBad("Expected true", *inposp).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn burn_false(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
if *inposp + 5 <= input.len() && &input[*inposp..*inposp + 5] == b"false" {
|
||||
*inposp += 5;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChorusError::JsonBad("Expected false", *inposp).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn burn_number(input: &[u8], inposp: &mut usize) -> Result<(), Error> {
|
||||
// For burning, we don't check validity.
|
||||
while *inposp < input.len() && b".+-0123456789abcdefABCDEF_oOxXn".contains(&input[*inposp]) {
|
||||
*inposp += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
pub mod json_escape;
|
||||
|
||||
pub mod json_parse;
|
||||
|
||||
pub mod utf8;
|
||||
|
||||
#[inline]
|
||||
pub fn put(output: &mut [u8], offset: usize, data: &[u8]) -> Result<(), crate::error::Error> {
|
||||
if output.len() < offset + data.len() {
|
||||
Err(crate::error::ChorusError::BufferTooSmall.into())
|
||||
} else {
|
||||
output[offset..offset + data.len()].copy_from_slice(data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,165 +0,0 @@
|
||||
use crate::error::{ChorusError, Error};
|
||||
|
||||
// Reads the next code point if UTF-8, and returns it along with the number of characters
|
||||
// that make it up.
|
||||
pub fn next_code_point(input: &[u8]) -> Result<Option<(u32, usize)>, Error> {
|
||||
let len = input.len();
|
||||
if len < 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Decode UTF-8
|
||||
let x = input[0];
|
||||
if x < 128 {
|
||||
return Ok(Some((x as u32, 1)));
|
||||
}
|
||||
|
||||
// Multibyte case follows
|
||||
// Decode from a byte combination out of: [[[x y] z] w]
|
||||
let init = utf8_first_byte(x, 2);
|
||||
if len < 2 {
|
||||
return Err(ChorusError::Utf8Error.into());
|
||||
}
|
||||
let y = input[1];
|
||||
let mut ch = utf8_acc_cont_byte(init, y);
|
||||
if x >= 0xE0 {
|
||||
// [[x y z] w] case
|
||||
// 5th bit in 0xE0 .. 0xEF is always clear, so `init` is still valid
|
||||
if len < 3 {
|
||||
return Err(ChorusError::Utf8Error.into());
|
||||
}
|
||||
let z = input[2];
|
||||
let y_z = utf8_acc_cont_byte((y & CONT_MASK) as u32, z);
|
||||
ch = init << 12 | y_z;
|
||||
if x >= 0xF0 {
|
||||
// [x y z w] case
|
||||
// use only the lower 3 bits of `init`
|
||||
if len < 4 {
|
||||
return Err(ChorusError::Utf8Error.into());
|
||||
}
|
||||
let w = input[3];
|
||||
ch = (init & 7) << 18 | utf8_acc_cont_byte(y_z, w);
|
||||
Ok(Some((ch, 4)))
|
||||
} else {
|
||||
Ok(Some((ch, 3)))
|
||||
}
|
||||
} else {
|
||||
Ok(Some((ch, 2)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_utf8(code: u32, dst: &mut [u8]) -> Result<usize, Error> {
|
||||
// UTF-8 ranges and tags for encoding characters
|
||||
const TAG_CONT: u8 = 0b1000_0000;
|
||||
const TAG_TWO_B: u8 = 0b1100_0000;
|
||||
const TAG_THREE_B: u8 = 0b1110_0000;
|
||||
const TAG_FOUR_B: u8 = 0b1111_0000;
|
||||
const MAX_ONE_B: u32 = 0x80;
|
||||
const MAX_TWO_B: u32 = 0x800;
|
||||
const MAX_THREE_B: u32 = 0x10000;
|
||||
|
||||
let len = unsafe {
|
||||
if code < MAX_ONE_B && !dst.is_empty() {
|
||||
*dst.get_unchecked_mut(0) = code as u8;
|
||||
1
|
||||
} else if code < MAX_TWO_B && dst.len() >= 2 {
|
||||
*dst.get_unchecked_mut(0) = (code >> 6 & 0x1F) as u8 | TAG_TWO_B;
|
||||
*dst.get_unchecked_mut(1) = (code & 0x3F) as u8 | TAG_CONT;
|
||||
2
|
||||
} else if code < MAX_THREE_B && dst.len() >= 3 {
|
||||
*dst.get_unchecked_mut(0) = (code >> 12 & 0x0F) as u8 | TAG_THREE_B;
|
||||
*dst.get_unchecked_mut(1) = (code >> 6 & 0x3F) as u8 | TAG_CONT;
|
||||
*dst.get_unchecked_mut(2) = (code & 0x3F) as u8 | TAG_CONT;
|
||||
3
|
||||
} else if dst.len() >= 4 {
|
||||
*dst.get_unchecked_mut(0) = (code >> 18 & 0x07) as u8 | TAG_FOUR_B;
|
||||
*dst.get_unchecked_mut(1) = (code >> 12 & 0x3F) as u8 | TAG_CONT;
|
||||
*dst.get_unchecked_mut(2) = (code >> 6 & 0x3F) as u8 | TAG_CONT;
|
||||
*dst.get_unchecked_mut(3) = (code & 0x3F) as u8 | TAG_CONT;
|
||||
4
|
||||
} else {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
};
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
/// Returns the initial codepoint accumulator for the first byte.
|
||||
/// The first byte is special, only want bottom 5 bits for width 2, 4 bits
|
||||
/// for width 3, and 3 bits for width 4.
|
||||
#[inline]
|
||||
const fn utf8_first_byte(byte: u8, width: u32) -> u32 {
|
||||
(byte & (0x7F >> width)) as u32
|
||||
}
|
||||
|
||||
/// Returns the value of `ch` updated with continuation byte `byte`.
|
||||
#[inline]
|
||||
const fn utf8_acc_cont_byte(ch: u32, byte: u8) -> u32 {
|
||||
(ch << 6) | (byte & CONT_MASK) as u32
|
||||
}
|
||||
|
||||
/// Mask of the value bits of a continuation byte.
|
||||
const CONT_MASK: u8 = 0b0011_1111;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{encode_utf8, next_code_point};
|
||||
|
||||
#[test]
|
||||
fn test_next_code_point() {
|
||||
let (codepoint, size) = next_code_point(r#"𝄞"#.as_bytes()).unwrap().unwrap();
|
||||
assert_eq!(codepoint, 119070);
|
||||
assert_eq!(size, 4);
|
||||
|
||||
let (codepoint, size) = next_code_point(r#"†"#.as_bytes()).unwrap().unwrap();
|
||||
assert_eq!(codepoint, 0x2020);
|
||||
assert_eq!(size, 3);
|
||||
|
||||
// four codepoints
|
||||
let s = [
|
||||
0x61, 0xE0, 0xA4, 0xA8, 0xE0, 0xA4, 0xBF, 0xE4, 0xBA, 0x9C, 0xF0, 0x90, 0x82, 0x83,
|
||||
];
|
||||
let (codepoint, size) = next_code_point(s.as_slice()).unwrap().unwrap();
|
||||
assert_eq!(codepoint, 0x61);
|
||||
assert_eq!(size, 1);
|
||||
|
||||
let mut start = size;
|
||||
let (codepoint, size) = next_code_point(&s[start..]).unwrap().unwrap();
|
||||
assert_eq!(codepoint, 0x928);
|
||||
assert_eq!(size, 3);
|
||||
|
||||
start += size;
|
||||
let (codepoint, size) = next_code_point(&s[start..]).unwrap().unwrap();
|
||||
assert_eq!(codepoint, 0x93F);
|
||||
assert_eq!(size, 3);
|
||||
|
||||
start += size;
|
||||
let (codepoint, size) = next_code_point(&s[start..]).unwrap().unwrap();
|
||||
assert_eq!(codepoint, 0x4E9C);
|
||||
assert_eq!(size, 3);
|
||||
|
||||
start += size;
|
||||
let (codepoint, size) = next_code_point(&s[start..]).unwrap().unwrap();
|
||||
assert_eq!(codepoint, 0x10083);
|
||||
assert_eq!(size, 4);
|
||||
|
||||
assert_eq!(next_code_point(&s[0..0]).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_utf8() {
|
||||
let mut buffer: Vec<u8> = vec![0, 0, 0, 0];
|
||||
|
||||
assert_eq!(encode_utf8(0x69, &mut buffer).unwrap(), 1);
|
||||
assert_eq!(buffer[0], 0x69);
|
||||
|
||||
assert_eq!(encode_utf8(0xEC, &mut buffer).unwrap(), 2);
|
||||
assert_eq!(&buffer[0..2], &[0xC3, 0xAC]);
|
||||
|
||||
assert_eq!(encode_utf8(0x5450, &mut buffer).unwrap(), 3);
|
||||
assert_eq!(&buffer[0..3], &[0xE5, 0x91, 0x90]);
|
||||
|
||||
assert_eq!(encode_utf8(0x2825F, &mut buffer).unwrap(), 4);
|
||||
assert_eq!(&buffer[..], &[0xF0, 0xA8, 0x89, 0x9F]);
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
use crate::error::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct Pubkey(pub [u8; 32]);
|
||||
|
||||
impl Pubkey {
|
||||
pub fn write_hex(&self, output: &mut [u8]) -> Result<(), Error> {
|
||||
write_hex!(self.0, output, 32)
|
||||
}
|
||||
|
||||
pub fn read_hex(input: &[u8]) -> Result<Pubkey, Error> {
|
||||
let mut out: [u8; 32] = [0; 32];
|
||||
read_hex!(input, &mut out, 32)?;
|
||||
Ok(Pubkey(out))
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
self.0.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Pubkey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut bytes: [u8; 64] = [0; 64];
|
||||
self.write_hex(&mut bytes).unwrap();
|
||||
let hex = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
write!(f, "{hex}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Pubkey;
|
||||
|
||||
#[test]
|
||||
fn test_pubkey_hex_functions() {
|
||||
let hex = b"1110ee4ff957fa9c55832eaccb4dc1c45bfc6304e1e4e9fa478f53df4b20062d";
|
||||
let pubkey = Pubkey::read_hex(hex).unwrap();
|
||||
eprintln!("{:?}", pubkey);
|
||||
let mut hex2: [u8; 64] = [0; 64];
|
||||
pubkey.write_hex(&mut hex2).unwrap();
|
||||
assert_eq!(hex, &hex2);
|
||||
assert_eq!(format!("{}", pubkey).as_bytes(), hex);
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
use crate::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Sig(pub [u8; 64]);
|
||||
|
||||
impl Sig {
|
||||
pub fn write_hex(&self, output: &mut [u8]) -> Result<(), Error> {
|
||||
write_hex!(self.0, output, 64)
|
||||
}
|
||||
|
||||
pub fn read_hex(input: &[u8]) -> Result<Sig, Error> {
|
||||
let mut out: [u8; 64] = [0; 64];
|
||||
read_hex!(input, &mut out, 64)?;
|
||||
Ok(Sig(out))
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
self.0.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Sig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut bytes: [u8; 128] = [0; 128];
|
||||
self.write_hex(&mut bytes).unwrap();
|
||||
let hex = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
write!(f, "{hex}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Sig;
|
||||
|
||||
#[test]
|
||||
fn test_sig_hex_functions() {
|
||||
let hex = b"f4165cd621d387e0f723c3ca7484ca3da9ede00ffc97eb57c3e695384e095dea1a6215e7328b793e878f436f508006f95957c7e6b652e80d4c3c47b9f9110e7d";
|
||||
let sig = Sig::read_hex(hex).unwrap();
|
||||
eprintln!("{:?}", sig);
|
||||
let mut hex2: [u8; 128] = [0; 128];
|
||||
sig.write_hex(&mut hex2).unwrap();
|
||||
assert_eq!(hex, &hex2);
|
||||
assert_eq!(format!("{}", sig).as_bytes(), hex);
|
||||
}
|
||||
}
|
||||
@ -1,332 +0,0 @@
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::types::parse::json_parse::read_tags_array;
|
||||
use std::fmt;
|
||||
|
||||
/*
|
||||
* 0 .. 2 u16 Length of the tags section
|
||||
* 2 .. 4 u16 num_tags
|
||||
* 4 .. 6 u16 offset of zeroeth tag
|
||||
* 6 .. 8 u16 offset of first tag
|
||||
* ...
|
||||
*
|
||||
* 4+num_tags*2 .. beginning of actual tag data
|
||||
*
|
||||
* Tag data looks like this for each tag:
|
||||
* count, (len, data), (len, data), ...
|
||||
*/
|
||||
|
||||
/// This stores an array of tags, each tag being an array of byte-strings.
|
||||
/// It is stored in a single packed linear byte array.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tags<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> Tags<'a> {
|
||||
// this marks off the slice of bytes that represent the tags from a potentially longer input
|
||||
pub fn delineate(input: &'a [u8]) -> Result<Tags<'a>, Error> {
|
||||
if input.len() < 2 {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
let len = parse_u16!(input, 0) as usize;
|
||||
if input.len() < len {
|
||||
return Err(ChorusError::EndOfInput.into());
|
||||
}
|
||||
Ok(Tags(&input[0..len]))
|
||||
}
|
||||
|
||||
/// Parse JSON input into a Tags.
|
||||
///
|
||||
/// Returns the count of consumed input bytes and the Tags
|
||||
pub fn from_json(json: &[u8], output_buffer: &'a mut [u8]) -> Result<(usize, Tags<'a>), Error> {
|
||||
let mut inpos: usize = 0;
|
||||
let tags_size = read_tags_array(json, &mut inpos, output_buffer)?;
|
||||
Ok((inpos, Tags(&output_buffer[..tags_size])))
|
||||
}
|
||||
|
||||
// This copies
|
||||
pub fn copy(&self, output: &mut [u8]) -> Result<(), Error> {
|
||||
if output.len() < self.0.len() {
|
||||
return Err(ChorusError::BufferTooSmall.into());
|
||||
}
|
||||
output[..self.0.len()].copy_from_slice(self.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
parse_u16!(self.0, 2) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn iter(&'a self) -> TagsIter<'a> {
|
||||
TagsIter {
|
||||
tags: self,
|
||||
next: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_string(&'a self, tag: usize, string: usize) -> Option<&'a [u8]> {
|
||||
if tag >= self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut offset = parse_u16!(self.0, 4 + tag * 2) as usize;
|
||||
let count = parse_u16!(self.0, offset) as usize;
|
||||
offset += 2;
|
||||
if string >= count {
|
||||
return None;
|
||||
}
|
||||
|
||||
let end = parse_u16!(self.0, 0) as usize;
|
||||
|
||||
// pass the fields we aren't reading
|
||||
for _ in 0..string {
|
||||
let len = parse_u16!(self.0, offset) as usize;
|
||||
offset += 2 + len;
|
||||
if offset > end {
|
||||
// safety check
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let len = parse_u16!(self.0, offset) as usize;
|
||||
offset += 2;
|
||||
if offset + len > end {
|
||||
// safety check
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(&self.0[offset..offset + len])
|
||||
}
|
||||
|
||||
pub fn get_value(&'a self, key: &[u8]) -> Option<&'a [u8]> {
|
||||
for tag in 0..self.len() {
|
||||
if let Some(thing) = self.get_string(tag, 0) {
|
||||
if thing == key {
|
||||
return self.get_string(tag, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn matches(&self, letter: &[u8], value: &[u8]) -> bool {
|
||||
for mut tag in self.iter() {
|
||||
if tag.next() == Some(letter) && tag.next() == Some(value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn as_json(&self) -> Vec<u8> {
|
||||
let mut output: Vec<u8> = Vec::with_capacity(256);
|
||||
let mut first = true;
|
||||
output.push(b'[');
|
||||
for tag in self.iter() {
|
||||
if !first {
|
||||
output.push(b',');
|
||||
}
|
||||
output.push(b'[');
|
||||
let mut firststring = true;
|
||||
for bytes in tag {
|
||||
if !firststring {
|
||||
output.push(b',');
|
||||
}
|
||||
output.push(b'"');
|
||||
output.extend(bytes);
|
||||
output.push(b'"');
|
||||
firststring = false;
|
||||
}
|
||||
output.push(b']');
|
||||
first = false;
|
||||
}
|
||||
output.push(b']');
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Tags<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let bytes = self.as_json();
|
||||
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
write!(f, "{s}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TagsIter<'a> {
|
||||
tags: &'a Tags<'a>,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TagsIter<'a> {
|
||||
type Item = TagsStringIter<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.next >= self.tags.len() {
|
||||
None
|
||||
} else {
|
||||
let offset_slot = 4 + self.next * 2;
|
||||
let offset = parse_u16!(self.tags.0, offset_slot) as usize;
|
||||
let count = parse_u16!(self.tags.0, offset) as usize;
|
||||
self.next += 1;
|
||||
Some(TagsStringIter {
|
||||
tags: self.tags,
|
||||
count,
|
||||
cur_offset: offset + 2,
|
||||
next: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TagsStringIter<'a> {
|
||||
tags: &'a Tags<'a>,
|
||||
count: usize,
|
||||
cur_offset: usize,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TagsStringIter<'a> {
|
||||
type Item = &'a [u8];
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.next >= self.count {
|
||||
None
|
||||
} else {
|
||||
// Read len
|
||||
let len = parse_u16!(self.tags.0, self.cur_offset) as usize;
|
||||
let s = &self.tags.0[self.cur_offset + 2..self.cur_offset + 2 + len];
|
||||
self.cur_offset += 2 + len;
|
||||
self.next += 1;
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Tags;
|
||||
|
||||
#[test]
|
||||
fn test_tag() {
|
||||
/*
|
||||
* [
|
||||
* ["Hello world!", "Hello", "world!"],
|
||||
* ["p", ""ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"],
|
||||
* ]
|
||||
*/
|
||||
|
||||
let data: Vec<u8> = vec![
|
||||
110, 0, // tags_len
|
||||
2, 0, // num_tags
|
||||
8, 0, // first tag at offset 8
|
||||
39, 0, // second tag at offset 39
|
||||
// 8:
|
||||
3, 0, // three fields long
|
||||
12, 0, // first field 12 bytes long
|
||||
72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, // "Hello world!"
|
||||
5, 0, // second field 5 bytes long
|
||||
72, 101, 108, 108, 111, // "Hello"
|
||||
6, 0, // third field 6 bytes long
|
||||
119, 111, 114, 108, 100, 33, // world!
|
||||
// 39:
|
||||
2, 0, // two fields long
|
||||
1, 0, // first field 1 bytes long
|
||||
112, // "p"
|
||||
64, 0, // second field 64 bytes long
|
||||
101, 101, 49, 49, 97, 53, 100, 102, 102, 52, 48, 99, 49, 57, 97, 53, 53, 53, 102, 52,
|
||||
49, 102, 101, 52, 50, 98, 52, 56, 102, 48, 48, 101, 54, 49, 56, 99, 57, 49, 50, 50, 53,
|
||||
54, 50, 50, 97, 101, 51, 55, 98, 54, 99, 50, 98, 98, 54, 55, 98, 55, 54, 99, 52, 101,
|
||||
52, 57,
|
||||
// "ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"
|
||||
];
|
||||
|
||||
let tags = Tags::delineate(&data).unwrap();
|
||||
|
||||
// Test tag access with get_string()
|
||||
|
||||
assert_eq!(tags.get_string(0, 0), Some(b"Hello world!".as_slice()));
|
||||
assert_eq!(tags.get_string(0, 1), Some(b"Hello".as_slice()));
|
||||
assert_eq!(tags.get_string(0, 2), Some(b"world!".as_slice()));
|
||||
assert_eq!(tags.get_string(0, 3), None);
|
||||
assert_eq!(tags.get_string(1, 0), Some(b"p".as_slice()));
|
||||
assert_eq!(
|
||||
tags.get_string(1, 1),
|
||||
Some(b"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49".as_slice())
|
||||
);
|
||||
assert_eq!(tags.get_string(1, 2), None);
|
||||
assert_eq!(tags.get_string(2, 0), None);
|
||||
|
||||
// Test tag access with iterators
|
||||
|
||||
let mut iter = tags.iter();
|
||||
|
||||
let mut tag1 = iter.next().unwrap();
|
||||
println!("TagsStringIter 1 {:?}", tag1);
|
||||
assert_eq!(tag1.next(), Some(b"Hello world!".as_slice()));
|
||||
assert_eq!(tag1.next(), Some(b"Hello".as_slice()));
|
||||
assert_eq!(tag1.next(), Some(b"world!".as_slice()));
|
||||
assert!(tag1.next().is_none());
|
||||
|
||||
let mut tag2 = iter.next().unwrap();
|
||||
assert_eq!(tag2.next(), Some(b"p".as_slice()));
|
||||
assert_eq!(
|
||||
tag2.next(),
|
||||
Some(b"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49".as_slice())
|
||||
);
|
||||
assert!(tag2.next().is_none());
|
||||
|
||||
let tag3 = iter.next();
|
||||
assert!(tag3.is_none());
|
||||
|
||||
assert_eq!(
|
||||
format!("{tags}"),
|
||||
r#"[["Hello world!","Hello","world!"],["p","ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"]]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tags_from_json() {
|
||||
let mut output: Vec<u8> = Vec::with_capacity(4096);
|
||||
output.resize(4096, 0);
|
||||
|
||||
let json = r#"[["Hello world!","Hello","world!"],["p","ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"]]"#;
|
||||
|
||||
let (_, tags) = Tags::from_json(json.as_bytes(), &mut output).unwrap();
|
||||
|
||||
assert_eq!(tags.get_string(0, 0), Some(b"Hello world!".as_slice()));
|
||||
assert_eq!(tags.get_string(0, 1), Some(b"Hello".as_slice()));
|
||||
assert_eq!(tags.get_string(0, 2), Some(b"world!".as_slice()));
|
||||
assert_eq!(tags.get_string(0, 3), None);
|
||||
assert_eq!(tags.get_string(1, 0), Some(b"p".as_slice()));
|
||||
assert_eq!(
|
||||
tags.get_string(1, 1),
|
||||
Some(b"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49".as_slice())
|
||||
);
|
||||
assert_eq!(tags.get_string(1, 2), None);
|
||||
assert_eq!(tags.get_string(2, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_tag() {
|
||||
let mut output: Vec<u8> = Vec::with_capacity(256);
|
||||
output.resize(256, 0);
|
||||
|
||||
let json = r#"[[]]"#;
|
||||
let (_, _tags) = Tags::from_json(json.as_bytes(), &mut output).unwrap();
|
||||
|
||||
let json = r#"[["-"],[]]"#;
|
||||
let (_, tags) = Tags::from_json(json.as_bytes(), &mut output).unwrap();
|
||||
assert_eq!(tags.get_string(0, 0), Some(b"-".as_slice()));
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
use speedy::{Readable, Writable};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Readable, Writable)]
|
||||
pub struct Time(pub u64);
|
||||
|
||||
impl Default for Time {
|
||||
fn default() -> Time {
|
||||
Time::now()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Time {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Time {
|
||||
pub fn as_u64(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn min() -> Time {
|
||||
Time(0)
|
||||
}
|
||||
|
||||
pub fn max() -> Time {
|
||||
Time(u64::MAX)
|
||||
}
|
||||
|
||||
pub fn now() -> Time {
|
||||
// Safety: unwrap() can only panic if the system time is before UNIX_EPOCH
|
||||
Time(std::time::UNIX_EPOCH.elapsed().unwrap().as_secs())
|
||||
}
|
||||
}
|
||||
173
src/bin/chorus.rs
Normal file
173
src/bin/chorus.rs
Normal file
@ -0,0 +1,173 @@
|
||||
use chorus::config::{Config, FriendlyConfig};
|
||||
use chorus::error::Error;
|
||||
use chorus::globals::GLOBALS;
|
||||
use chorus::ip::HashedPeer;
|
||||
use chorus::tls::MaybeTlsStream;
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus <config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
let config = chorus::load_config(&config_path)?;
|
||||
|
||||
chorus::setup_logging(&config);
|
||||
|
||||
// Log host name
|
||||
log::info!(target: "Server", "HOSTNAME = {}", config.hostname);
|
||||
|
||||
let store = chorus::setup_store(&config)?;
|
||||
let _ = GLOBALS.store.set(store);
|
||||
|
||||
// TLS setup
|
||||
let maybe_tls_acceptor = if config.use_tls {
|
||||
log::info!(target: "Server", "Using TLS");
|
||||
Some(chorus::tls::tls_acceptor(&config)?)
|
||||
} else {
|
||||
log::info!(target: "Server", "Not using TLS");
|
||||
None
|
||||
};
|
||||
|
||||
// Bind listener to port
|
||||
let listener = TcpListener::bind((&*config.ip_address, config.port)).await?;
|
||||
log::info!(target: "Server", "Running on {}:{}", config.ip_address, config.port);
|
||||
|
||||
// Store config into GLOBALS
|
||||
*GLOBALS.config.write() = config;
|
||||
|
||||
let mut interrupt_signal = signal(SignalKind::interrupt())?;
|
||||
let mut quit_signal = signal(SignalKind::quit())?;
|
||||
let mut terminate_signal = signal(SignalKind::terminate())?;
|
||||
let mut hup_signal = signal(SignalKind::hangup())?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Exits gracefully upon exit-type signals
|
||||
v = interrupt_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGINT");
|
||||
break;
|
||||
},
|
||||
v = quit_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGQUIT");
|
||||
break;
|
||||
},
|
||||
v = terminate_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGTERM");
|
||||
break;
|
||||
},
|
||||
|
||||
// Reload config on HUP
|
||||
v = hup_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGHUP: Reloading configuration");
|
||||
|
||||
// Reload the config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path.clone())?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let config: Config = friendly_config.into_config()?;
|
||||
|
||||
*GLOBALS.config.write() = config;
|
||||
|
||||
chorus::print_stats();
|
||||
},
|
||||
|
||||
// Accepts network connections and spawn a task to serve each one
|
||||
v = listener.accept() => {
|
||||
let (tcp_stream, hashed_peer) = {
|
||||
let (tcp_stream, peer_addr) = v?;
|
||||
let hashed_peer = HashedPeer::new(peer_addr);
|
||||
(tcp_stream, hashed_peer)
|
||||
};
|
||||
|
||||
// Possibly IP block
|
||||
if GLOBALS.config.read().enable_ip_blocking {
|
||||
let ip_data = chorus::get_ip_data(GLOBALS.store.get().unwrap(), hashed_peer.ip())?;
|
||||
if ip_data.is_banned() {
|
||||
log::debug!(target: "Client",
|
||||
"{}: Blocking reconnection until {}",
|
||||
hashed_peer.ip(),
|
||||
ip_data.ban_until);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tls_acceptor) = &maybe_tls_acceptor {
|
||||
let tls_acceptor_clone = tls_acceptor.clone();
|
||||
tokio::spawn(async move {
|
||||
match tls_acceptor_clone.accept(tcp_stream).await {
|
||||
Err(e) => log::error!(
|
||||
target: "Client",
|
||||
"{}: {}", hashed_peer, e
|
||||
),
|
||||
Ok(tls_stream) => {
|
||||
if let Err(e) = chorus::serve(MaybeTlsStream::Rustls(tls_stream), hashed_peer).await {
|
||||
log::error!(
|
||||
target: "Client",
|
||||
"{}: {}", hashed_peer, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
chorus::serve(MaybeTlsStream::Plain(tcp_stream), hashed_peer).await?;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Pre-sync in case something below hangs up
|
||||
let _ = GLOBALS.store.get().unwrap().sync();
|
||||
|
||||
// Set the shutting down signal
|
||||
let _ = GLOBALS.shutting_down.send(true);
|
||||
|
||||
// Wait for active websockets to shutdown gracefully
|
||||
let mut num_clients = GLOBALS.num_clients.load(Ordering::Relaxed);
|
||||
if num_clients != 0 {
|
||||
log::info!(target: "Server", "Waiting for {num_clients} websockets to shutdown...");
|
||||
|
||||
// We will check if all clients have shutdown every 25ms
|
||||
let interval = tokio::time::interval(Duration::from_millis(25));
|
||||
tokio::pin!(interval);
|
||||
|
||||
while num_clients != 0 {
|
||||
// If we get another shutdown signal, stop waiting for websockets
|
||||
tokio::select! {
|
||||
v = interrupt_signal.recv() => if v.is_some() {
|
||||
break;
|
||||
},
|
||||
v = quit_signal.recv() => if v.is_some() {
|
||||
break;
|
||||
},
|
||||
v = terminate_signal.recv() => if v.is_some() {
|
||||
break;
|
||||
},
|
||||
_instant = interval.tick() => {
|
||||
num_clients = GLOBALS.num_clients.load(Ordering::Relaxed);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(target: "Server", "Syncing and shutting down.");
|
||||
let _ = GLOBALS.store.get().unwrap().sync();
|
||||
|
||||
chorus::print_stats();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
22
src/bin/chorus_compress.rs
Normal file
22
src/bin/chorus_compress.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use chorus::error::Error;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus_compress <config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
let config = chorus::load_config(config_path)?;
|
||||
|
||||
chorus::setup_logging(&config);
|
||||
|
||||
let store = chorus::setup_store(&config)?;
|
||||
|
||||
let _store = unsafe { store.rebuild()? };
|
||||
|
||||
Ok(())
|
||||
}
|
||||
43
src/bin/chorus_dump.rs
Normal file
43
src/bin/chorus_dump.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use chorus::error::Error;
|
||||
use pocket_types::{Event, Filter};
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus_dump <chorus_config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
let mut config = chorus::load_config(config_path)?;
|
||||
|
||||
// Force allow of scraping (this program is a scraper)
|
||||
config.allow_scraping = true;
|
||||
|
||||
chorus::setup_logging(&config);
|
||||
|
||||
// Setup store
|
||||
let store = chorus::setup_store(&config)?;
|
||||
|
||||
let mut buffer: [u8; 128] = [0; 128];
|
||||
let (_incount, _outcount, filter) = Filter::from_json(b"{}", &mut buffer)?;
|
||||
let screen = |_: &Event| -> bool { true };
|
||||
|
||||
let mut events = store.find_events(
|
||||
filter,
|
||||
config.allow_scraping,
|
||||
config.allow_scrape_if_limited_to,
|
||||
config.allow_scrape_if_max_seconds,
|
||||
screen,
|
||||
)?;
|
||||
|
||||
for event in events.drain(..) {
|
||||
let bytes = event.as_json()?;
|
||||
let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
|
||||
println!("{s}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
32
src/bin/chorus_dump_approvals.rs
Normal file
32
src/bin/chorus_dump_approvals.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use chorus::error::Error;
|
||||
use std::env;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus_moderate <config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
let mut config = chorus::load_config(config_path)?;
|
||||
|
||||
// Force allow of scraping (this program is a scraper)
|
||||
config.allow_scraping = true;
|
||||
|
||||
chorus::setup_logging(&config);
|
||||
|
||||
// Setup store
|
||||
let store = chorus::setup_store(&config)?;
|
||||
|
||||
for (id, approved) in chorus::dump_event_approvals(&store)? {
|
||||
println!("ID {} = {}", id, approved);
|
||||
}
|
||||
|
||||
for (pubkey, approved) in chorus::dump_pubkey_approvals(&store)? {
|
||||
println!("PUBKEY {} = {}", pubkey, approved);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,10 +1,7 @@
|
||||
use chorus_lib::config::{Config, FriendlyConfig};
|
||||
use chorus_lib::error::Error;
|
||||
use chorus_lib::store::Store;
|
||||
use chorus_lib::types::{Event, Filter, Kind};
|
||||
use chorus::error::Error;
|
||||
use pocket_types::{Event, Filter, Kind};
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Read, Write};
|
||||
use std::io::Write;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
@ -15,35 +12,25 @@ fn main() -> Result<(), Error> {
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
// Read config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let mut config: Config = friendly_config.into_config()?;
|
||||
|
||||
env_logger::Builder::new()
|
||||
.filter_level(config.library_log_level)
|
||||
.filter(Some("Server"), config.server_log_level)
|
||||
.filter(Some("Client"), config.client_log_level)
|
||||
.format_target(true)
|
||||
.format_module_path(false)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
log::debug!(target: "Server", "Loaded config file.");
|
||||
|
||||
let mut config = chorus::load_config(config_path)?;
|
||||
// Force allow of scraping (this program is a scraper)
|
||||
config.allow_scraping = true;
|
||||
|
||||
// Setup store
|
||||
let store = Store::new(&config)?;
|
||||
chorus::setup_logging(&config);
|
||||
|
||||
let store = chorus::setup_store(&config)?;
|
||||
|
||||
let mut buffer: [u8; 128] = [0; 128];
|
||||
let (_incount, _outcount, filter) = Filter::from_json(b"{}", &mut buffer)?;
|
||||
let screen = |_: &Event<'_>| -> bool { true };
|
||||
let screen = |_: &Event| -> bool { true };
|
||||
|
||||
let mut events = store.find_events(filter, screen, &config)?;
|
||||
let mut events = store.find_events(
|
||||
filter,
|
||||
config.allow_scraping,
|
||||
config.allow_scrape_if_limited_to,
|
||||
config.allow_scrape_if_max_seconds,
|
||||
screen,
|
||||
)?;
|
||||
|
||||
let mut stdout = std::io::stdout();
|
||||
let stdin = std::io::stdin();
|
||||
@ -51,12 +38,12 @@ fn main() -> Result<(), Error> {
|
||||
|
||||
'eventloop: for event in events.drain(..) {
|
||||
// Skip DMs which don't need approval
|
||||
if event.kind() == Kind(4) || event.kind() == Kind(1059) {
|
||||
if event.kind() == Kind::from(4) || event.kind() == Kind::from(1059) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip relay lists which don't need approval
|
||||
if event.kind() == Kind(10002) {
|
||||
if event.kind() == Kind::from(10002) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -75,18 +62,27 @@ fn main() -> Result<(), Error> {
|
||||
//println!("{s}");
|
||||
|
||||
// Skip if event marked approved
|
||||
if matches!(store.get_event_approval(event.id()), Ok(Some(true))) {
|
||||
if matches!(
|
||||
chorus::get_event_approval(&store, event.id()),
|
||||
Ok(Some(true))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if pubkey marked approved
|
||||
if matches!(store.get_pubkey_approval(event.pubkey()), Ok(Some(true))) {
|
||||
if matches!(
|
||||
chorus::get_pubkey_approval(&store, event.pubkey()),
|
||||
Ok(Some(true))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete if pubkey marked banned
|
||||
if matches!(store.get_pubkey_approval(event.pubkey()), Ok(Some(false))) {
|
||||
store.delete_event(event.id())?;
|
||||
if matches!(
|
||||
chorus::get_pubkey_approval(&store, event.pubkey()),
|
||||
Ok(Some(false))
|
||||
) {
|
||||
store.remove_event(event.id())?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -109,24 +105,24 @@ fn main() -> Result<(), Error> {
|
||||
}
|
||||
match input.bytes().next().unwrap() {
|
||||
b'p' => {
|
||||
store.mark_pubkey_approval(event.pubkey(), true)?;
|
||||
chorus::mark_pubkey_approval(&store, event.pubkey(), true)?;
|
||||
println!("User approved.");
|
||||
break;
|
||||
}
|
||||
b'P' => {
|
||||
store.mark_pubkey_approval(event.pubkey(), false)?;
|
||||
store.delete_event(event.id())?;
|
||||
chorus::mark_pubkey_approval(&store, event.pubkey(), false)?;
|
||||
store.remove_event(event.id())?;
|
||||
println!("User banned.");
|
||||
break;
|
||||
}
|
||||
b'i' => {
|
||||
store.mark_event_approval(event.id(), true)?;
|
||||
chorus::mark_event_approval(&store, event.id(), true)?;
|
||||
println!("Event approved.");
|
||||
break;
|
||||
}
|
||||
b'I' => {
|
||||
store.mark_event_approval(event.id(), false)?;
|
||||
store.delete_event(event.id())?;
|
||||
chorus::mark_event_approval(&store, event.id(), false)?;
|
||||
store.remove_event(event.id())?;
|
||||
println!("Event banned.");
|
||||
break;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::error::Error;
|
||||
use crate::types::Pubkey;
|
||||
use pocket_types::Pubkey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use url::Host;
|
||||
@ -47,7 +47,7 @@ pub enum ChorusError {
|
||||
ChannelRecv(tokio::sync::broadcast::error::RecvError),
|
||||
|
||||
// Channel Send
|
||||
ChannelSend(tokio::sync::broadcast::error::SendError<usize>),
|
||||
ChannelSend(tokio::sync::broadcast::error::SendError<u64>),
|
||||
|
||||
// Config
|
||||
Config(toml::de::Error),
|
||||
@ -100,8 +100,8 @@ pub enum ChorusError {
|
||||
// JSON Escape Surrogate
|
||||
JsonEscapeSurrogate,
|
||||
|
||||
// LMDB
|
||||
Lmdb(heed::Error),
|
||||
// Missing Table
|
||||
MissingTable(&'static str),
|
||||
|
||||
// No private key
|
||||
NoPrivateKey,
|
||||
@ -112,6 +112,15 @@ pub enum ChorusError {
|
||||
// Protected Event
|
||||
ProtectedEvent,
|
||||
|
||||
// Pocket Db Error
|
||||
PocketDb(pocket_db::Error),
|
||||
|
||||
// Pocket Db Heed Error
|
||||
PocketDbHeed(pocket_db::heed::Error),
|
||||
|
||||
// Pocket Types Error
|
||||
PocketType(pocket_types::Error),
|
||||
|
||||
// Range error
|
||||
RangeError,
|
||||
|
||||
@ -190,9 +199,12 @@ impl std::fmt::Display for ChorusError {
|
||||
f,
|
||||
"JSON string escape surrogate (ancient style) is not supported"
|
||||
),
|
||||
ChorusError::Lmdb(e) => write!(f, "{e}"),
|
||||
ChorusError::MissingTable(t) => write!(f, "Missing table: {t}"),
|
||||
ChorusError::NoPrivateKey => write!(f, "Private Key Not Found"),
|
||||
ChorusError::NoSuchSubscription => write!(f, "No such subscription"),
|
||||
ChorusError::PocketDb(e) => write!(f, "{e}"),
|
||||
ChorusError::PocketDbHeed(e) => write!(f, "{e}"),
|
||||
ChorusError::PocketType(e) => write!(f, "{e}"),
|
||||
ChorusError::ProtectedEvent => write!(f, "Protected event"),
|
||||
ChorusError::RangeError => write!(f, "Range error"),
|
||||
ChorusError::Restricted => write!(f, "Restricted"),
|
||||
@ -220,7 +232,9 @@ impl StdError for ChorusError {
|
||||
ChorusError::Http(e) => Some(e),
|
||||
ChorusError::Hyper(e) => Some(e),
|
||||
ChorusError::Io(e) => Some(e),
|
||||
ChorusError::Lmdb(e) => Some(e),
|
||||
ChorusError::PocketDb(e) => Some(e),
|
||||
ChorusError::PocketDbHeed(e) => Some(e),
|
||||
ChorusError::PocketType(e) => Some(e),
|
||||
ChorusError::Rustls(e) => Some(e),
|
||||
ChorusError::Speedy(e) => Some(e),
|
||||
ChorusError::Tungstenite(e) => Some(e),
|
||||
@ -261,9 +275,12 @@ impl ChorusError {
|
||||
ChorusError::JsonBadStringChar(_) => 0.2,
|
||||
ChorusError::JsonEscape => 0.2,
|
||||
ChorusError::JsonEscapeSurrogate => 0.05,
|
||||
ChorusError::Lmdb(_) => 0.0,
|
||||
ChorusError::MissingTable(_) => 0.0,
|
||||
ChorusError::NoPrivateKey => 0.0,
|
||||
ChorusError::NoSuchSubscription => 0.05,
|
||||
ChorusError::PocketDb(_) => 0.0,
|
||||
ChorusError::PocketDbHeed(_) => 0.0,
|
||||
ChorusError::PocketType(_) => 0.0,
|
||||
ChorusError::ProtectedEvent => 0.35,
|
||||
ChorusError::RangeError => 0.0,
|
||||
ChorusError::Restricted => 0.1,
|
||||
@ -308,9 +325,9 @@ impl From<tokio::sync::broadcast::error::RecvError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tokio::sync::broadcast::error::SendError<usize>> for Error {
|
||||
impl From<tokio::sync::broadcast::error::SendError<u64>> for Error {
|
||||
#[track_caller]
|
||||
fn from(err: tokio::sync::broadcast::error::SendError<usize>) -> Self {
|
||||
fn from(err: tokio::sync::broadcast::error::SendError<u64>) -> Self {
|
||||
Error {
|
||||
inner: ChorusError::ChannelSend(err),
|
||||
location: std::panic::Location::caller(),
|
||||
@ -368,11 +385,31 @@ impl From<std::io::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<heed::Error> for Error {
|
||||
impl From<pocket_db::Error> for Error {
|
||||
#[track_caller]
|
||||
fn from(err: heed::Error) -> Self {
|
||||
fn from(err: pocket_db::Error) -> Self {
|
||||
Error {
|
||||
inner: ChorusError::Lmdb(err),
|
||||
inner: ChorusError::PocketDb(err),
|
||||
location: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pocket_db::heed::Error> for Error {
|
||||
#[track_caller]
|
||||
fn from(err: pocket_db::heed::Error) -> Self {
|
||||
Error {
|
||||
inner: ChorusError::PocketDbHeed(err),
|
||||
location: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pocket_types::Error> for Error {
|
||||
#[track_caller]
|
||||
fn from(err: pocket_types::Error) -> Self {
|
||||
Error {
|
||||
inner: ChorusError::PocketType(err),
|
||||
location: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
use chorus_lib::config::Config;
|
||||
use chorus_lib::store::Store;
|
||||
use crate::config::Config;
|
||||
use hyper::server::conn::Http;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use pocket_db::Store;
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Instant;
|
||||
@ -22,7 +22,7 @@ pub struct Globals {
|
||||
/// Every handler needs to listen to it and check if the incoming event matches any
|
||||
/// subscribed fitlers for their client, and if so, send the event to their client under
|
||||
/// that subscription.
|
||||
pub new_events: BroadcastSender<usize>,
|
||||
pub new_events: BroadcastSender<u64>,
|
||||
|
||||
pub num_clients: AtomicUsize,
|
||||
pub shutting_down: WatchSender<bool>,
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::types::Time;
|
||||
use pocket_types::Time;
|
||||
use speedy::{Readable, Writable};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
@ -118,7 +118,7 @@ impl IpReputation {
|
||||
// Memory-only short-term record of IP handling
|
||||
#[derive(Debug, Clone, Default, Readable, Writable)]
|
||||
pub struct IpData {
|
||||
pub ban_until: Time,
|
||||
pub ban_until: u64,
|
||||
pub reputation: IpReputation,
|
||||
}
|
||||
|
||||
@ -134,15 +134,15 @@ impl IpData {
|
||||
// Compute ban_until
|
||||
let mut until = Time::now();
|
||||
let seconds = self.ban_seconds(session_exit, minimum_ban_seconds);
|
||||
until.0 += seconds;
|
||||
until = until + seconds;
|
||||
|
||||
self.ban_until = Time(self.ban_until.0.max(until.0));
|
||||
self.ban_until = self.ban_until.max(until.as_u64());
|
||||
|
||||
seconds
|
||||
}
|
||||
|
||||
pub fn is_banned(&self) -> bool {
|
||||
self.ban_until > Time::now()
|
||||
Time::from_u64(self.ban_until) > Time::now()
|
||||
}
|
||||
|
||||
fn ban_seconds(&self, session_exit: SessionExit, minimum_ban_seconds: u64) -> u64 {
|
||||
@ -1,240 +1,46 @@
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod globals;
|
||||
pub mod ip;
|
||||
pub mod nostr;
|
||||
pub mod reply;
|
||||
pub mod tls;
|
||||
pub mod web;
|
||||
|
||||
use crate::config::{Config, FriendlyConfig};
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::globals::GLOBALS;
|
||||
use crate::ip::{HashedIp, HashedPeer, IpData, SessionExit};
|
||||
use crate::reply::NostrReply;
|
||||
use crate::tls::MaybeTlsStream;
|
||||
use chorus_lib::config::{Config, FriendlyConfig};
|
||||
use chorus_lib::error::{ChorusError, Error};
|
||||
use chorus_lib::ip::{HashedIp, HashedPeer, SessionExit};
|
||||
use chorus_lib::reply::NostrReply;
|
||||
use chorus_lib::store::Store;
|
||||
use chorus_lib::types::{OwnedFilter, Pubkey};
|
||||
use futures::{sink::SinkExt, stream::StreamExt};
|
||||
use hyper::service::Service;
|
||||
use hyper::upgrade::Upgraded;
|
||||
use hyper::{Body, Request, Response};
|
||||
use hyper_tungstenite::{tungstenite, WebSocketStream};
|
||||
use hyper_tungstenite::tungstenite;
|
||||
use hyper_tungstenite::WebSocketStream;
|
||||
use pocket_db::Store;
|
||||
use pocket_types::{Id, OwnedFilter, Pubkey};
|
||||
use speedy::{Readable, Writable};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::error::Error as StdError;
|
||||
use std::fs::OpenOptions;
|
||||
use std::future::Future;
|
||||
use std::io::Read;
|
||||
use std::net::IpAddr;
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
use textnonce::TextNonce;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
use tungstenite::protocol::WebSocketConfig;
|
||||
use tungstenite::Message;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
// Get args (config path)
|
||||
let mut args = env::args();
|
||||
if args.len() <= 1 {
|
||||
panic!("USAGE: chorus <config_path>");
|
||||
}
|
||||
let _ = args.next(); // ignore program name
|
||||
let config_path = args.next().unwrap();
|
||||
|
||||
// Read config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path.clone())?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let config: Config = friendly_config.into_config()?;
|
||||
|
||||
env_logger::Builder::new()
|
||||
.filter_level(config.library_log_level)
|
||||
.filter(Some("Server"), config.server_log_level)
|
||||
.filter(Some("Client"), config.client_log_level)
|
||||
.format_target(true)
|
||||
.format_module_path(false)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
log::debug!(target: "Server", "Loaded config file.");
|
||||
|
||||
// Log host name
|
||||
log::info!(target: "Server", "HOSTNAME = {}", config.hostname);
|
||||
|
||||
// Setup store
|
||||
let store = Store::new(&config)?;
|
||||
let _ = GLOBALS.store.set(store);
|
||||
|
||||
// TLS setup
|
||||
let maybe_tls_acceptor = if config.use_tls {
|
||||
log::info!(target: "Server", "Using TLS");
|
||||
Some(tls::tls_acceptor(&config)?)
|
||||
} else {
|
||||
log::info!(target: "Server", "Not using TLS");
|
||||
None
|
||||
};
|
||||
|
||||
// Bind listener to port
|
||||
let listener = TcpListener::bind((&*config.ip_address, config.port)).await?;
|
||||
log::info!(target: "Server", "Running on {}:{}", config.ip_address, config.port);
|
||||
|
||||
// Store config into GLOBALS
|
||||
*GLOBALS.config.write() = config;
|
||||
|
||||
let mut interrupt_signal = signal(SignalKind::interrupt())?;
|
||||
let mut quit_signal = signal(SignalKind::quit())?;
|
||||
let mut terminate_signal = signal(SignalKind::terminate())?;
|
||||
let mut hup_signal = signal(SignalKind::hangup())?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Exits gracefully upon exit-type signals
|
||||
v = interrupt_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGINT");
|
||||
break;
|
||||
},
|
||||
v = quit_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGQUIT");
|
||||
break;
|
||||
},
|
||||
v = terminate_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGTERM");
|
||||
break;
|
||||
},
|
||||
|
||||
// Reload config on HUP
|
||||
v = hup_signal.recv() => if v.is_some() {
|
||||
log::info!(target: "Server", "SIGHUP: Reloading configuration");
|
||||
|
||||
// Reload the config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path.clone())?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let config: Config = friendly_config.into_config()?;
|
||||
|
||||
*GLOBALS.config.write() = config;
|
||||
|
||||
print_stats();
|
||||
},
|
||||
|
||||
// Accepts network connections and spawn a task to serve each one
|
||||
v = listener.accept() => {
|
||||
let (tcp_stream, hashed_peer) = {
|
||||
let (tcp_stream, peer_addr) = v?;
|
||||
let hashed_peer = HashedPeer::new(peer_addr);
|
||||
(tcp_stream, hashed_peer)
|
||||
};
|
||||
|
||||
// Possibly IP block
|
||||
if GLOBALS.config.read().enable_ip_blocking {
|
||||
let ip_data = GLOBALS.store.get().unwrap().get_ip_data(hashed_peer.ip())?;
|
||||
if ip_data.is_banned() {
|
||||
log::debug!(target: "Client",
|
||||
"{}: Blocking reconnection until {}",
|
||||
hashed_peer.ip(),
|
||||
ip_data.ban_until);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tls_acceptor) = &maybe_tls_acceptor {
|
||||
let tls_acceptor_clone = tls_acceptor.clone();
|
||||
tokio::spawn(async move {
|
||||
match tls_acceptor_clone.accept(tcp_stream).await {
|
||||
Err(e) => log::error!(
|
||||
target: "Client",
|
||||
"{}: {}", hashed_peer, e
|
||||
),
|
||||
Ok(tls_stream) => {
|
||||
if let Err(e) = serve(MaybeTlsStream::Rustls(tls_stream), hashed_peer).await {
|
||||
log::error!(
|
||||
target: "Client",
|
||||
"{}: {}", hashed_peer, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
serve(MaybeTlsStream::Plain(tcp_stream), hashed_peer).await?;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Pre-sync in case something below hangs up
|
||||
let _ = GLOBALS.store.get().unwrap().sync();
|
||||
|
||||
// Set the shutting down signal
|
||||
let _ = GLOBALS.shutting_down.send(true);
|
||||
|
||||
// Wait for active websockets to shutdown gracefully
|
||||
let mut num_clients = GLOBALS.num_clients.load(Ordering::Relaxed);
|
||||
if num_clients != 0 {
|
||||
log::info!(target: "Server", "Waiting for {num_clients} websockets to shutdown...");
|
||||
|
||||
// We will check if all clients have shutdown every 25ms
|
||||
let interval = tokio::time::interval(Duration::from_millis(25));
|
||||
tokio::pin!(interval);
|
||||
|
||||
while num_clients != 0 {
|
||||
// If we get another shutdown signal, stop waiting for websockets
|
||||
tokio::select! {
|
||||
v = interrupt_signal.recv() => if v.is_some() {
|
||||
break;
|
||||
},
|
||||
v = quit_signal.recv() => if v.is_some() {
|
||||
break;
|
||||
},
|
||||
v = terminate_signal.recv() => if v.is_some() {
|
||||
break;
|
||||
},
|
||||
_instant = interval.tick() => {
|
||||
num_clients = GLOBALS.num_clients.load(Ordering::Relaxed);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(target: "Server", "Syncing and shutting down.");
|
||||
let _ = GLOBALS.store.get().unwrap().sync();
|
||||
|
||||
print_stats();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_stats() {
|
||||
let mut runtime: u64 = GLOBALS.start_time.elapsed().as_secs();
|
||||
if runtime < 1 {
|
||||
runtime = 1;
|
||||
}
|
||||
log::info!(
|
||||
target: "Server",
|
||||
"Runtime: {} seconds", runtime
|
||||
);
|
||||
log::info!(
|
||||
target: "Server",
|
||||
"Inbound: {} bytes ({} B/s)",
|
||||
GLOBALS.bytes_inbound.load(Ordering::Relaxed),
|
||||
(GLOBALS.bytes_inbound.load(Ordering::Relaxed) as f32) / (runtime as f32)
|
||||
);
|
||||
log::info!(
|
||||
target: "Server",
|
||||
"Outbound: {} bytes ({} B/s)",
|
||||
GLOBALS.bytes_outbound.load(Ordering::Relaxed),
|
||||
(GLOBALS.bytes_outbound.load(Ordering::Relaxed) as f32) / (runtime as f32)
|
||||
);
|
||||
}
|
||||
|
||||
// Serve a single network connection
|
||||
async fn serve(stream: MaybeTlsStream<TcpStream>, peer: HashedPeer) -> Result<(), Error> {
|
||||
/// Serve a single network connection
|
||||
pub async fn serve(stream: MaybeTlsStream<TcpStream>, peer: HashedPeer) -> Result<(), Error> {
|
||||
// Serve the network stream with our http server and our HttpService
|
||||
let service = HttpService { peer };
|
||||
|
||||
@ -397,14 +203,10 @@ async fn handle_http_request(
|
||||
// if GLOBALS.config.read().enable_ip_blocking {
|
||||
let mut ban_seconds = 0;
|
||||
let minimum_ban_seconds = GLOBALS.config.read().minimum_ban_seconds;
|
||||
if let Ok(mut ip_data) = GLOBALS.store.get().unwrap().get_ip_data(peer.ip()) {
|
||||
if let Ok(mut ip_data) = get_ip_data(GLOBALS.store.get().unwrap(), peer.ip()) {
|
||||
ban_seconds =
|
||||
ip_data.update_on_session_close(session_exit, minimum_ban_seconds);
|
||||
let _ = GLOBALS
|
||||
.store
|
||||
.get()
|
||||
.unwrap()
|
||||
.update_ip_data(peer.ip(), &ip_data);
|
||||
let _ = update_ip_data(GLOBALS.store.get().unwrap(), peer.ip(), &ip_data);
|
||||
}
|
||||
|
||||
// we cheat somewhat and log these websocket open and close messages
|
||||
@ -512,7 +314,7 @@ impl WebSocketService {
|
||||
}
|
||||
|
||||
// If the event matches a subscription they have open, send them the event
|
||||
async fn handle_new_event(&mut self, new_event_offset: usize) -> Result<(), Error> {
|
||||
async fn handle_new_event(&mut self, new_event_offset: u64) -> Result<(), Error> {
|
||||
if self.subscriptions.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@ -523,13 +325,13 @@ impl WebSocketService {
|
||||
.unwrap()
|
||||
.get_event_by_offset(new_event_offset)?;
|
||||
|
||||
let event_flags = nostr::event_flags(&event, &self.user);
|
||||
let event_flags = nostr::event_flags(event, &self.user);
|
||||
let authorized_user = nostr::authorized_user(&self.user);
|
||||
|
||||
'subs: for (subid, filters) in self.subscriptions.iter() {
|
||||
for filter in filters.iter() {
|
||||
if filter.as_filter()?.event_matches(&event)?
|
||||
&& nostr::screen_outgoing_event(&event, &event_flags, authorized_user)
|
||||
if filter.event_matches(event)?
|
||||
&& nostr::screen_outgoing_event(event, &event_flags, authorized_user)
|
||||
{
|
||||
let message = NostrReply::Event(subid, event);
|
||||
self.websocket
|
||||
@ -609,3 +411,220 @@ impl WebSocketService {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Print statistics
|
||||
pub fn print_stats() {
|
||||
let mut runtime: u64 = GLOBALS.start_time.elapsed().as_secs();
|
||||
if runtime < 1 {
|
||||
runtime = 1;
|
||||
}
|
||||
log::info!(
|
||||
target: "Server",
|
||||
"Runtime: {} seconds", runtime
|
||||
);
|
||||
log::info!(
|
||||
target: "Server",
|
||||
"Inbound: {} bytes ({} B/s)",
|
||||
GLOBALS.bytes_inbound.load(Ordering::Relaxed),
|
||||
(GLOBALS.bytes_inbound.load(Ordering::Relaxed) as f32) / (runtime as f32)
|
||||
);
|
||||
log::info!(
|
||||
target: "Server",
|
||||
"Outbound: {} bytes ({} B/s)",
|
||||
GLOBALS.bytes_outbound.load(Ordering::Relaxed),
|
||||
(GLOBALS.bytes_outbound.load(Ordering::Relaxed) as f32) / (runtime as f32)
|
||||
);
|
||||
}
|
||||
|
||||
/// Load config file
|
||||
pub fn load_config<P: AsRef<Path>>(config_path: P) -> Result<Config, Error> {
|
||||
// Read config file
|
||||
let mut file = OpenOptions::new().read(true).open(config_path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let friendly_config: FriendlyConfig = toml::from_str(&contents)?;
|
||||
let config: Config = friendly_config.into_config()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Setup logging
|
||||
pub fn setup_logging(config: &Config) {
|
||||
env_logger::Builder::new()
|
||||
.filter_level(config.library_log_level)
|
||||
.filter(Some("Server"), config.server_log_level)
|
||||
.filter(Some("Client"), config.client_log_level)
|
||||
.format_target(true)
|
||||
.format_module_path(false)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
log::debug!(target: "Server", "Loaded config file.");
|
||||
}
|
||||
|
||||
/// Setup storage
|
||||
pub fn setup_store(config: &Config) -> Result<Store, Error> {
|
||||
let store = Store::new(
|
||||
&config.data_directory,
|
||||
vec![
|
||||
"approved-events", // id.as_slice() -> u8(bool)
|
||||
"approved-pubkeys", // pubkey.as_slice() -> u8(bool)
|
||||
"ip_data", // HashedIp.0 -> IpData
|
||||
],
|
||||
)?;
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// Get IpData from storage about this remote HashedIp
|
||||
pub fn get_ip_data(store: &Store, ip: HashedIp) -> Result<IpData, Error> {
|
||||
let ip_data = store
|
||||
.extra_table("ip_data")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable("ip_data")))?;
|
||||
let txn = store.read_txn()?;
|
||||
let key = &ip.0;
|
||||
let bytes = match ip_data.get(&txn, key)? {
|
||||
Some(b) => b,
|
||||
None => return Ok(Default::default()),
|
||||
};
|
||||
Ok(IpData::read_from_buffer(bytes)?)
|
||||
}
|
||||
|
||||
/// Get IpData in storage about this remote HashedIp
|
||||
pub fn update_ip_data(store: &Store, ip: HashedIp, data: &IpData) -> Result<(), Error> {
|
||||
let ip_data = store
|
||||
.extra_table("ip_data")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable("ip_data")))?;
|
||||
let mut txn = store.write_txn()?;
|
||||
let key = &ip.0;
|
||||
let bytes = data.write_to_vec()?;
|
||||
ip_data.put(&mut txn, key, &bytes)?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dump all IpData from storage
|
||||
pub fn dump_ip_data(store: &Store) -> Result<Vec<(HashedIp, IpData)>, Error> {
|
||||
let ip_data = store
|
||||
.extra_table("ip_data")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable("ip_data")))?;
|
||||
let txn = store.read_txn()?;
|
||||
let mut output: Vec<(HashedIp, IpData)> = Vec::new();
|
||||
for i in ip_data.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let hashedip = HashedIp::from_bytes(key);
|
||||
let data = IpData::read_from_buffer(val)?;
|
||||
output.push((hashedip, data));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Mark an event as approved or not
|
||||
pub fn mark_event_approval(store: &Store, id: Id, approval: bool) -> Result<(), Error> {
|
||||
let approved_events = store
|
||||
.extra_table("approved-events")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-events",
|
||||
)))?;
|
||||
let mut txn = store.write_txn()?;
|
||||
approved_events.put(&mut txn, id.as_slice(), &[approval as u8])?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear an event approval status
|
||||
pub fn clear_event_approval(store: &Store, id: Id) -> Result<(), Error> {
|
||||
let approved_events = store
|
||||
.extra_table("approved-events")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-events",
|
||||
)))?;
|
||||
let mut txn = store.write_txn()?;
|
||||
approved_events.delete(&mut txn, id.as_slice())?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch an event approval status
|
||||
pub fn get_event_approval(store: &Store, id: Id) -> Result<Option<bool>, Error> {
|
||||
let approved_events = store
|
||||
.extra_table("approved-events")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-events",
|
||||
)))?;
|
||||
let txn = store.read_txn()?;
|
||||
Ok(approved_events.get(&txn, id.as_slice())?.map(|u| u[0] != 0)) // FIXME in case data is zero length this will panic
|
||||
}
|
||||
|
||||
/// Dump all event approval statuses
|
||||
pub fn dump_event_approvals(store: &Store) -> Result<Vec<(Id, bool)>, Error> {
|
||||
let mut output: Vec<(Id, bool)> = Vec::new();
|
||||
let approved_events = store
|
||||
.extra_table("approved-events")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-events",
|
||||
)))?;
|
||||
let txn = store.read_txn()?;
|
||||
for i in approved_events.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let id = Id::from_bytes(key.try_into().unwrap());
|
||||
let approval: bool = val[0] != 0; // FIXME in case data is zero length this will panic
|
||||
output.push((id, approval));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Mark a pubkey as approved or not
|
||||
pub fn mark_pubkey_approval(store: &Store, pubkey: Pubkey, approval: bool) -> Result<(), Error> {
|
||||
let approved_pubkeys = store
|
||||
.extra_table("approved-pubkeys")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-pubkeys",
|
||||
)))?;
|
||||
let mut txn = store.write_txn()?;
|
||||
approved_pubkeys.put(&mut txn, pubkey.as_slice(), &[approval as u8])?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear a pubkey approval status
|
||||
pub fn clear_pubkey_approval(store: &Store, pubkey: Pubkey) -> Result<(), Error> {
|
||||
let approved_pubkeys = store
|
||||
.extra_table("approved-pubkeys")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-pubkeys",
|
||||
)))?;
|
||||
let mut txn = store.write_txn()?;
|
||||
approved_pubkeys.delete(&mut txn, pubkey.as_slice())?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch a pubkey approval status
|
||||
pub fn get_pubkey_approval(store: &Store, pubkey: Pubkey) -> Result<Option<bool>, Error> {
|
||||
let approved_pubkeys = store
|
||||
.extra_table("approved-pubkeys")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-pubkeys",
|
||||
)))?;
|
||||
let txn = store.read_txn()?;
|
||||
Ok(approved_pubkeys
|
||||
.get(&txn, pubkey.as_slice())?
|
||||
.map(|u| u[0] != 0)) // FIXME in case data is zero length this will panic
|
||||
}
|
||||
|
||||
/// Dump all pubkey approval statuses
|
||||
pub fn dump_pubkey_approvals(store: &Store) -> Result<Vec<(Pubkey, bool)>, Error> {
|
||||
let mut output: Vec<(Pubkey, bool)> = Vec::new();
|
||||
let approved_pubkeys = store
|
||||
.extra_table("approved-pubkeys")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable(
|
||||
"approved-pubkeys",
|
||||
)))?;
|
||||
let txn = store.read_txn()?;
|
||||
for i in approved_pubkeys.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let pubkey = Pubkey::from_bytes(key.try_into().unwrap());
|
||||
let approval: bool = val[0] != 0; // FIXME in case data is zero length this will panic
|
||||
output.push((pubkey, approval));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
@ -1,12 +1,11 @@
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::globals::GLOBALS;
|
||||
use crate::reply::{NostrReply, NostrReplyPrefix};
|
||||
use crate::WebSocketService;
|
||||
use chorus_lib::error::{ChorusError, Error};
|
||||
use chorus_lib::reply::{NostrReply, NostrReplyPrefix};
|
||||
use chorus_lib::types::parse::json_escape::json_unescape;
|
||||
use chorus_lib::types::parse::json_parse::*;
|
||||
use chorus_lib::types::{Event, Filter, Kind, OwnedFilter, Pubkey, Time};
|
||||
use futures::SinkExt;
|
||||
use hyper_tungstenite::tungstenite::Message;
|
||||
use pocket_types::json::{eat_whitespace, json_unescape, verify_char};
|
||||
use pocket_types::{Event, Filter, Kind, OwnedFilter, Pubkey, Time};
|
||||
use url::Url;
|
||||
|
||||
impl WebSocketService {
|
||||
@ -75,8 +74,7 @@ impl WebSocketService {
|
||||
inpos += incount;
|
||||
outpos += outcount;
|
||||
|
||||
let filterbytes = filter.as_bytes().to_owned();
|
||||
filters.push(OwnedFilter(filterbytes));
|
||||
filters.push(filter.to_owned());
|
||||
}
|
||||
|
||||
if let Err(e) = self.req_inner(&subid, filters).await {
|
||||
@ -116,12 +114,14 @@ impl WebSocketService {
|
||||
if user.is_none() {
|
||||
for filter in filters.iter() {
|
||||
// If any DM kinds were requested
|
||||
if filter.as_filter()?.num_kinds() == 0
|
||||
|| filter.as_filter()?.kinds().any(|k| k.0 == 4 || k.0 == 1059)
|
||||
if filter.num_kinds() == 0
|
||||
|| filter
|
||||
.kinds()
|
||||
.any(|k| k.as_u16() == 4 || k.as_u16() == 1059)
|
||||
{
|
||||
// They need to AUTH first to request DMs
|
||||
let reply = NostrReply::Closed(
|
||||
&subid,
|
||||
subid,
|
||||
NostrReplyPrefix::AuthRequired,
|
||||
"DM kinds were included in the REQ".to_owned(),
|
||||
);
|
||||
@ -140,7 +140,7 @@ impl WebSocketService {
|
||||
|
||||
// Serve events matching subscription
|
||||
{
|
||||
let mut events: Vec<Event> = Vec::new();
|
||||
let mut events: Vec<&Event> = Vec::new();
|
||||
|
||||
for filter in filters.iter() {
|
||||
let screen = |event: &Event| {
|
||||
@ -149,11 +149,13 @@ impl WebSocketService {
|
||||
};
|
||||
let filter_events = {
|
||||
let config = &*GLOBALS.config.read();
|
||||
GLOBALS
|
||||
.store
|
||||
.get()
|
||||
.unwrap()
|
||||
.find_events(filter.as_filter()?, screen, config)?
|
||||
GLOBALS.store.get().unwrap().find_events(
|
||||
filter,
|
||||
config.allow_scraping,
|
||||
config.allow_scrape_if_limited_to,
|
||||
config.allow_scrape_if_max_seconds,
|
||||
screen,
|
||||
)?
|
||||
};
|
||||
events.extend(filter_events);
|
||||
}
|
||||
@ -261,9 +263,9 @@ impl WebSocketService {
|
||||
let authorized_user = authorized_user(&user);
|
||||
|
||||
// Delineate the event back out of the session buffer
|
||||
let event = Event::delineate(&self.buffer)?;
|
||||
let event = unsafe { Event::delineate(&self.buffer)? };
|
||||
|
||||
let event_flags = event_flags(&event, &user);
|
||||
let event_flags = event_flags(event, &user);
|
||||
|
||||
if !event_flags.author_is_an_authorized_user || GLOBALS.config.read().verify_events {
|
||||
// Verify the event is valid (id is hash, signature is valid)
|
||||
@ -273,7 +275,7 @@ impl WebSocketService {
|
||||
}
|
||||
|
||||
// Screen the event to see if we are willing to accept it
|
||||
if !screen_incoming_event(&event, event_flags, authorized_user).await? {
|
||||
if !screen_incoming_event(event, event_flags, authorized_user).await? {
|
||||
if self.user.is_some() {
|
||||
return Err(ChorusError::Restricted.into());
|
||||
} else {
|
||||
@ -282,7 +284,7 @@ impl WebSocketService {
|
||||
}
|
||||
|
||||
// Store and index the event
|
||||
let offset = GLOBALS.store.get().unwrap().store_event(&event)?;
|
||||
let offset = GLOBALS.store.get().unwrap().store_event(event)?;
|
||||
GLOBALS.new_events.send(offset)?; // advertise the new event
|
||||
|
||||
Ok(())
|
||||
@ -349,14 +351,14 @@ impl WebSocketService {
|
||||
|
||||
async fn auth_inner(&mut self) -> Result<(), Error> {
|
||||
// Delineate the event back out of the session buffer
|
||||
let event = Event::delineate(&self.buffer)?;
|
||||
let event = unsafe { Event::delineate(&self.buffer)? };
|
||||
|
||||
// Verify the event (even if config.verify_events is off, because this is
|
||||
// strictly necessary for AUTH)
|
||||
event.verify()?;
|
||||
|
||||
// Verify the event is the right kind
|
||||
if event.kind() != Kind(22242) {
|
||||
if event.kind() != Kind::from(22242) {
|
||||
return Err(ChorusError::AuthFailure("Wrong event kind".to_string()).into());
|
||||
}
|
||||
|
||||
@ -402,7 +404,7 @@ impl WebSocketService {
|
||||
}
|
||||
|
||||
// Verify the created_at timestamp is within reason
|
||||
let timediff = (Time::now().0 as i64).abs_diff(event.created_at().0 as i64);
|
||||
let timediff = (Time::now().as_u64() as i64).abs_diff(event.created_at().as_u64() as i64);
|
||||
if timediff > 600 {
|
||||
return Err(
|
||||
ChorusError::AuthFailure("Time is more than 10 minutes off".to_string()).into(),
|
||||
@ -417,27 +419,17 @@ impl WebSocketService {
|
||||
}
|
||||
|
||||
async fn screen_incoming_event(
|
||||
event: &Event<'_>,
|
||||
event: &Event,
|
||||
event_flags: EventFlags,
|
||||
authorized_user: bool,
|
||||
) -> Result<bool, Error> {
|
||||
// Reject if event approval is false
|
||||
if let Some(false) = GLOBALS
|
||||
.store
|
||||
.get()
|
||||
.unwrap()
|
||||
.get_event_approval(event.id())?
|
||||
{
|
||||
if let Some(false) = crate::get_event_approval(GLOBALS.store.get().unwrap(), event.id())? {
|
||||
return Err(ChorusError::BannedEvent.into());
|
||||
}
|
||||
|
||||
// Reject if pubkey approval is false
|
||||
if let Some(false) = GLOBALS
|
||||
.store
|
||||
.get()
|
||||
.unwrap()
|
||||
.get_pubkey_approval(event.pubkey())?
|
||||
{
|
||||
if let Some(false) = crate::get_pubkey_approval(GLOBALS.store.get().unwrap(), event.pubkey())? {
|
||||
return Err(ChorusError::BannedUser.into());
|
||||
}
|
||||
|
||||
@ -463,7 +455,7 @@ async fn screen_incoming_event(
|
||||
}
|
||||
|
||||
// Accept relay lists from anybody
|
||||
if event.kind() == Kind(10002) && GLOBALS.config.read().serve_relay_lists {
|
||||
if event.kind() == Kind::from(10002) && GLOBALS.config.read().serve_relay_lists {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@ -494,19 +486,19 @@ async fn screen_incoming_event(
|
||||
}
|
||||
|
||||
pub fn screen_outgoing_event(
|
||||
event: &Event<'_>,
|
||||
event: &Event,
|
||||
event_flags: &EventFlags,
|
||||
authorized_user: bool,
|
||||
) -> bool {
|
||||
// Forbid if it is a private event (DM or GiftWrap) and theey are neither the recipient
|
||||
// nor the author
|
||||
if event.kind() == Kind(4) || event.kind() == Kind(1059) {
|
||||
if event.kind() == Kind::from(4) || event.kind() == Kind::from(1059) {
|
||||
return event_flags.tags_current_user || event_flags.author_is_current_user;
|
||||
}
|
||||
|
||||
// Forbid (and delete) if it has an expired expiration tag
|
||||
if matches!(event.is_expired(), Ok(true)) {
|
||||
let _ = GLOBALS.store.get().unwrap().delete_event(event.id());
|
||||
let _ = GLOBALS.store.get().unwrap().remove_event(event.id());
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -516,7 +508,7 @@ pub fn screen_outgoing_event(
|
||||
}
|
||||
|
||||
// Allow Relay Lists
|
||||
if event.kind() == Kind(10002) && GLOBALS.config.read().serve_relay_lists {
|
||||
if event.kind() == Kind::from(10002) && GLOBALS.config.read().serve_relay_lists {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -536,16 +528,12 @@ pub fn screen_outgoing_event(
|
||||
}
|
||||
|
||||
// Allow if event is explicitly approved
|
||||
if let Ok(Some(true)) = GLOBALS.store.get().unwrap().get_event_approval(event.id()) {
|
||||
if let Ok(Some(true)) = crate::get_event_approval(GLOBALS.store.get().unwrap(), event.id()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow if author is explicitly approved
|
||||
if let Ok(Some(true)) = GLOBALS
|
||||
.store
|
||||
.get()
|
||||
.unwrap()
|
||||
.get_pubkey_approval(event.pubkey())
|
||||
if let Ok(Some(true)) = crate::get_pubkey_approval(GLOBALS.store.get().unwrap(), event.pubkey())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -568,7 +556,7 @@ pub struct EventFlags {
|
||||
pub tags_current_user: bool,
|
||||
}
|
||||
|
||||
pub fn event_flags(event: &Event<'_>, user: &Option<Pubkey>) -> EventFlags {
|
||||
pub fn event_flags(event: &Event, user: &Option<Pubkey>) -> EventFlags {
|
||||
let author_is_an_authorized_user = GLOBALS.config.read().user_keys.contains(&event.pubkey());
|
||||
|
||||
let author_is_current_user = match user {
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::types::{Event, Id};
|
||||
use pocket_types::{Event, Id};
|
||||
use std::fmt;
|
||||
|
||||
pub enum NostrReplyPrefix {
|
||||
@ -31,7 +31,7 @@ impl fmt::Display for NostrReplyPrefix {
|
||||
|
||||
pub enum NostrReply<'a> {
|
||||
Auth(String),
|
||||
Event(&'a str, Event<'a>),
|
||||
Event(&'a str, &'a Event),
|
||||
Ok(Id, bool, NostrReplyPrefix, String),
|
||||
Eose(&'a str),
|
||||
Closed(&'a str, NostrReplyPrefix, String),
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::globals::GLOBALS;
|
||||
use chorus_lib::config::Config;
|
||||
use chorus_lib::error::{ChorusError, Error};
|
||||
use rustls::{Certificate, PrivateKey};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::Error;
|
||||
use crate::globals::GLOBALS;
|
||||
use chorus_lib::config::Config;
|
||||
use chorus_lib::error::Error;
|
||||
use chorus_lib::ip::HashedPeer;
|
||||
use crate::ip::HashedPeer;
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
|
||||
pub async fn serve_http(peer: HashedPeer, request: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
Loading…
x
Reference in New Issue
Block a user