From 76098f463edd48a0d5835c9e0a4a3472eaf69bb7 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Thu, 29 Aug 2024 21:05:36 +0100 Subject: [PATCH 1/7] Proof-of-concept device-profile struct + test-runner. This is incomplete, but does give an idea about the structure how device-profiles can be organized. --- Dockerfile | 7 + Makefile | 5 + docker-compose.yml | 6 + rust-toolchain.toml | 4 + shell.nix | 7 + test-runner/.gitignore | 1 + test-runner/Cargo.lock | 419 ++++ test-runner/Cargo.toml | 10 + test-runner/src/js/mod.rs | 64 + test-runner/src/js/vendor_base64_js.rs | 155 ++ test-runner/src/js/vendor_buffer.rs | 2119 +++++++++++++++++ test-runner/src/js/vendor_ieee754.rs | 110 + test-runner/src/main.rs | 108 + test-runner/src/structs.rs | 80 + vendors/example-template/codecs/example.js | 13 + .../codecs/test_decode_example.json | 13 + .../codecs/test_encode_example.json | 13 + vendors/example-template/devices/example.toml | 8 + .../profiles/example-EU868.toml | 23 + vendors/example-template/vendor.toml | 5 + 20 files changed, 3170 insertions(+) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 100644 rust-toolchain.toml create mode 100644 shell.nix create mode 100644 test-runner/.gitignore create mode 100644 test-runner/Cargo.lock create mode 100644 test-runner/Cargo.toml create mode 100644 test-runner/src/js/mod.rs create mode 100644 test-runner/src/js/vendor_base64_js.rs create mode 100644 test-runner/src/js/vendor_buffer.rs create mode 100644 test-runner/src/js/vendor_ieee754.rs create mode 100644 test-runner/src/main.rs create mode 100644 test-runner/src/structs.rs create mode 100644 vendors/example-template/codecs/example.js create mode 100644 vendors/example-template/codecs/test_decode_example.json create mode 100644 vendors/example-template/codecs/test_encode_example.json create mode 100644 vendors/example-template/devices/example.toml create mode 100644 vendors/example-template/profiles/example-EU868.toml create mode 100644 vendors/example-template/vendor.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3e8b2e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM nixos/nix + +ENV PROJECT_PATH=/lorawan-device-profiles +WORKDIR $PROJECT_PATH + +ENTRYPOINT ["nix-shell"] + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..40e2851 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +.PHONY: test + +test: + docker compose run --rm lorawan-device-profiles --run 'cd test-runner && cargo run' + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..85a9aa6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + lorawan-device-profiles: + build: + context: . + volumes: + - ./:/lorawan-device-profiles diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..be2896e --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] + channel = "1.80.1" + components = ["rustfmt", "clippy"] + profile = "default" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..bc8042b --- /dev/null +++ b/shell.nix @@ -0,0 +1,7 @@ +{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz") {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.rustup + ]; +} diff --git a/test-runner/.gitignore b/test-runner/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/test-runner/.gitignore @@ -0,0 +1 @@ +/target diff --git a/test-runner/Cargo.lock b/test-runner/Cargo.lock new file mode 100644 index 0000000..e7cedc3 --- /dev/null +++ b/test-runner/Cargo.lock @@ -0,0 +1,419 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "cc" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +dependencies = [ + "shlex", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rquickjs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cbd33e0b668aea0ab238b9164523aca929096f9f40834700d71d91dd4888882" +dependencies = [ + "rquickjs-core", +] + +[[package]] +name = "rquickjs-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9129d69b7b8f7ee8ad1da5b12c7f4a8a8acd45f2e6dd9cb2ee1bc5a1f2fa3d" +dependencies = [ + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6f2288d8e7fbb5130f62cf720451641e99d55f6fde9db86aa2914ecb553fd2" +dependencies = [ + "cc", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_valid" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c92fa3e7abc9108490d7f9ac752008e12297d5a969511a66fd0a42fbcaf1cd" +dependencies = [ + "indexmap", + "itertools", + "num-traits", + "once_cell", + "paste", + "regex", + "serde", + "serde_json", + "serde_valid_derive", + "serde_valid_literal", + "thiserror", + "unicode-segmentation", +] + +[[package]] +name = "serde_valid_derive" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02529dfc96005e7e0d2dd5a12a7d9664cf51be96a5b1a58a496e8080020711ee" +dependencies = [ + "itertools", + "paste", + "proc-macro-error", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.76", +] + +[[package]] +name = "serde_valid_literal" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b385e1d2d8fd49695e9ae387ebf9bde548fe8dcecd1cf5f164f9c2a2b4a3c3" +dependencies = [ + "paste", + "regex", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test-runner" +version = "0.1.0" +dependencies = [ + "rquickjs", + "serde", + "serde_valid", + "toml", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] diff --git a/test-runner/Cargo.toml b/test-runner/Cargo.toml new file mode 100644 index 0000000..bdeaa52 --- /dev/null +++ b/test-runner/Cargo.toml @@ -0,0 +1,10 @@ +[package] + name = "test-runner" + version = "0.1.0" + edition = "2021" + +[dependencies] + serde = { version = "1.0", features = ["derive"] } + serde_valid = "0.24" + toml = "0.8" + rquickjs = { version = "0.6", features = ["loader", "array-buffer"] } diff --git a/test-runner/src/js/mod.rs b/test-runner/src/js/mod.rs new file mode 100644 index 0000000..671d6a3 --- /dev/null +++ b/test-runner/src/js/mod.rs @@ -0,0 +1,64 @@ +use rquickjs::CatchResultExt; + +mod vendor_base64_js; +mod vendor_buffer; +mod vendor_ieee754; + +pub fn run_tests(function: &str, codec: &str, codec_tests: &str) { + let resolver = rquickjs::loader::BuiltinResolver::default() + .with_module("base64-js") + .with_module("ieee754") + .with_module("buffer"); + let loader = rquickjs::loader::BuiltinLoader::default() + .with_module("base64-js", vendor_base64_js::SCRIPT) + .with_module("ieee754", vendor_ieee754::SCRIPT) + .with_module("buffer", vendor_buffer::SCRIPT); + + let rt = rquickjs::Runtime::new().unwrap(); + rt.set_loader(resolver, loader); + + let ctx = rquickjs::Context::full(&rt).unwrap(); + + let script = format!( + r#" + {} + + const tests = {}; + + for (test of tests) {{ + const out = {}(test.input); + if (JSON.stringify(out) !== JSON.stringify(test.expected)) {{ + throw new Error("Test '"+ test.name +"' failed - Expected: " + JSON.stringify(test.expected) + " Got: " + JSON.stringify(out)); + }} + }} + "#, + codec, codec_tests, function + ); + + ctx.with(|ctx| { + let buff = rquickjs::Module::declare( + ctx.clone(), + "b", + r#" + import { Buffer } from "buffer"; + export { Buffer } + "#, + ) + .expect("Declare script"); + + let (buff, buff_promise) = buff.eval().catch(&ctx).expect("JS eval"); + let _ret: rquickjs::Value = buff_promise.finish().expect("Script finish"); + let buff: rquickjs::Function = buff.get("Buffer").expect("Get buffer"); + + let globals = ctx.globals(); + globals.set("Buffer", buff).expect("Set Buffer"); + + let mut eval_options = rquickjs::context::EvalOptions::default(); + eval_options.strict = false; + + let _ret: rquickjs::Value = ctx + .eval_with_options(script, eval_options) + .catch(&ctx) + .expect("Running tests"); + }); +} diff --git a/test-runner/src/js/vendor_base64_js.rs b/test-runner/src/js/vendor_base64_js.rs new file mode 100644 index 0000000..1f5a66a --- /dev/null +++ b/test-runner/src/js/vendor_base64_js.rs @@ -0,0 +1,155 @@ +// base64-js does basic base64 encoding/decoding in pure JS. +// +// License: MIT +// Source: https://github.com/beatgammit/base64-js + +pub const SCRIPT: &str = r#" +'use strict' + +export { byteLength, toByteArray, fromByteArray }; + +var lookup = [] +var revLookup = [] +var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array + +var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +for (var i = 0, len = code.length; i < len; ++i) { + lookup[i] = code[i] + revLookup[code.charCodeAt(i)] = i +} + +// Support decoding URL-safe base64 strings, as Node.js does. +// See: https://en.wikipedia.org/wiki/Base64#URL_applications +revLookup['-'.charCodeAt(0)] = 62 +revLookup['_'.charCodeAt(0)] = 63 + +function getLens (b64) { + var len = b64.length + + if (len % 4 > 0) { + throw new Error('Invalid string. Length must be a multiple of 4') + } + + // Trim off extra bytes after placeholder bytes are found + // See: https://github.com/beatgammit/base64-js/issues/42 + var validLen = b64.indexOf('=') + if (validLen === -1) validLen = len + + var placeHoldersLen = validLen === len + ? 0 + : 4 - (validLen % 4) + + return [validLen, placeHoldersLen] +} + +// base64 is 4/3 + up to two characters of the original data +function byteLength (b64) { + var lens = getLens(b64) + var validLen = lens[0] + var placeHoldersLen = lens[1] + return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen +} + +function _byteLength (b64, validLen, placeHoldersLen) { + return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen +} + +function toByteArray (b64) { + var tmp + var lens = getLens(b64) + var validLen = lens[0] + var placeHoldersLen = lens[1] + + var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) + + var curByte = 0 + + // if there are placeholders, only get up to the last complete 4 chars + var len = placeHoldersLen > 0 + ? validLen - 4 + : validLen + + var i + for (i = 0; i < len; i += 4) { + tmp = + (revLookup[b64.charCodeAt(i)] << 18) | + (revLookup[b64.charCodeAt(i + 1)] << 12) | + (revLookup[b64.charCodeAt(i + 2)] << 6) | + revLookup[b64.charCodeAt(i + 3)] + arr[curByte++] = (tmp >> 16) & 0xFF + arr[curByte++] = (tmp >> 8) & 0xFF + arr[curByte++] = tmp & 0xFF + } + + if (placeHoldersLen === 2) { + tmp = + (revLookup[b64.charCodeAt(i)] << 2) | + (revLookup[b64.charCodeAt(i + 1)] >> 4) + arr[curByte++] = tmp & 0xFF + } + + if (placeHoldersLen === 1) { + tmp = + (revLookup[b64.charCodeAt(i)] << 10) | + (revLookup[b64.charCodeAt(i + 1)] << 4) | + (revLookup[b64.charCodeAt(i + 2)] >> 2) + arr[curByte++] = (tmp >> 8) & 0xFF + arr[curByte++] = tmp & 0xFF + } + + return arr +} + +function tripletToBase64 (num) { + return lookup[num >> 18 & 0x3F] + + lookup[num >> 12 & 0x3F] + + lookup[num >> 6 & 0x3F] + + lookup[num & 0x3F] +} + +function encodeChunk (uint8, start, end) { + var tmp + var output = [] + for (var i = start; i < end; i += 3) { + tmp = + ((uint8[i] << 16) & 0xFF0000) + + ((uint8[i + 1] << 8) & 0xFF00) + + (uint8[i + 2] & 0xFF) + output.push(tripletToBase64(tmp)) + } + return output.join('') +} + +function fromByteArray (uint8) { + var tmp + var len = uint8.length + var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes + var parts = [] + var maxChunkLength = 16383 // must be multiple of 3 + + // go through the array every three bytes, we'll deal with trailing stuff later + for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { + parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) + } + + // pad the end with zeros, but make sure to not forget the extra bytes + if (extraBytes === 1) { + tmp = uint8[len - 1] + parts.push( + lookup[tmp >> 2] + + lookup[(tmp << 4) & 0x3F] + + '==' + ) + } else if (extraBytes === 2) { + tmp = (uint8[len - 2] << 8) + uint8[len - 1] + parts.push( + lookup[tmp >> 10] + + lookup[(tmp >> 4) & 0x3F] + + lookup[(tmp << 2) & 0x3F] + + '=' + ) + } + + return parts.join('') +} +"#; diff --git a/test-runner/src/js/vendor_buffer.rs b/test-runner/src/js/vendor_buffer.rs new file mode 100644 index 0000000..61b2d54 --- /dev/null +++ b/test-runner/src/js/vendor_buffer.rs @@ -0,0 +1,2119 @@ +// The buffer module from node.js, for the browser. +// +// License: MIT +// Source: https://github.com/feross/buffer + +pub const SCRIPT: &str = r#" +/*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + */ +/* eslint-disable no-proto */ + +'use strict' + +import * as base64 from "base64-js"; +import "ieee754"; + +const customInspectSymbol = + (typeof Symbol === 'function' && typeof Symbol['for'] === 'function') // eslint-disable-line dot-notation + ? Symbol['for']('nodejs.util.inspect.custom') // eslint-disable-line dot-notation + : null + +export { Buffer }; + +var exports = {}; +exports.Buffer = Buffer +exports.SlowBuffer = SlowBuffer +exports.INSPECT_MAX_BYTES = 50 + +const K_MAX_LENGTH = 0x7fffffff +exports.kMaxLength = K_MAX_LENGTH + +/** + * If `Buffer.TYPED_ARRAY_SUPPORT`: + * === true Use Uint8Array implementation (fastest) + * === false Print warning and recommend using `buffer` v4.x which has an Object + * implementation (most compatible, even IE6) + * + * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, + * Opera 11.6+, iOS 4.2+. + * + * We report that the browser does not support typed arrays if the are not subclassable + * using __proto__. Firefox 4-29 lacks support for adding new properties to `Uint8Array` + * (See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438). IE 10 lacks support + * for __proto__ and has a buggy typed array implementation. + */ +Buffer.TYPED_ARRAY_SUPPORT = typedArraySupport() + +if (!Buffer.TYPED_ARRAY_SUPPORT && typeof console !== 'undefined' && + typeof console.error === 'function') { + console.error( + 'This browser lacks typed array (Uint8Array) support which is required by ' + + '`buffer` v5.x. Use `buffer` v4.x if you require old browser support.' + ) +} + +function typedArraySupport () { + // Can typed array instances can be augmented? + try { + const arr = new Uint8Array(1) + const proto = { foo: function () { return 42 } } + Object.setPrototypeOf(proto, Uint8Array.prototype) + Object.setPrototypeOf(arr, proto) + return arr.foo() === 42 + } catch (e) { + return false + } +} + +Object.defineProperty(Buffer.prototype, 'parent', { + enumerable: true, + get: function () { + if (!Buffer.isBuffer(this)) return undefined + return this.buffer + } +}) + +Object.defineProperty(Buffer.prototype, 'offset', { + enumerable: true, + get: function () { + if (!Buffer.isBuffer(this)) return undefined + return this.byteOffset + } +}) + +function createBuffer (length) { + if (length > K_MAX_LENGTH) { + throw new RangeError('The value "' + length + '" is invalid for option "size"') + } + // Return an augmented `Uint8Array` instance + const buf = new Uint8Array(length) + Object.setPrototypeOf(buf, Buffer.prototype) + return buf +} + +/** + * The Buffer constructor returns instances of `Uint8Array` that have their + * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of + * `Uint8Array`, so the returned instances will have all the node `Buffer` methods + * and the `Uint8Array` methods. Square bracket notation works as expected -- it + * returns a single octet. + * + * The `Uint8Array` prototype remains unmodified. + */ + +function Buffer (arg, encodingOrOffset, length) { + // Common case. + if (typeof arg === 'number') { + if (typeof encodingOrOffset === 'string') { + throw new TypeError( + 'The "string" argument must be of type string. Received type number' + ) + } + return allocUnsafe(arg) + } + return from(arg, encodingOrOffset, length) +} + +Buffer.poolSize = 8192 // not used by this implementation + +function from (value, encodingOrOffset, length) { + if (typeof value === 'string') { + return fromString(value, encodingOrOffset) + } + + if (ArrayBuffer.isView(value)) { + return fromArrayView(value) + } + + if (value == null) { + throw new TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + + 'or Array-like Object. Received type ' + (typeof value) + ) + } + + if (isInstance(value, ArrayBuffer) || + (value && isInstance(value.buffer, ArrayBuffer))) { + return fromArrayBuffer(value, encodingOrOffset, length) + } + + if (typeof SharedArrayBuffer !== 'undefined' && + (isInstance(value, SharedArrayBuffer) || + (value && isInstance(value.buffer, SharedArrayBuffer)))) { + return fromArrayBuffer(value, encodingOrOffset, length) + } + + if (typeof value === 'number') { + throw new TypeError( + 'The "value" argument must not be of type number. Received type number' + ) + } + + const valueOf = value.valueOf && value.valueOf() + if (valueOf != null && valueOf !== value) { + return Buffer.from(valueOf, encodingOrOffset, length) + } + + const b = fromObject(value) + if (b) return b + + if (typeof Symbol !== 'undefined' && Symbol.toPrimitive != null && + typeof value[Symbol.toPrimitive] === 'function') { + return Buffer.from(value[Symbol.toPrimitive]('string'), encodingOrOffset, length) + } + + throw new TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' + + 'or Array-like Object. Received type ' + (typeof value) + ) +} + +/** + * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError + * if value is a number. + * Buffer.from(str[, encoding]) + * Buffer.from(array) + * Buffer.from(buffer) + * Buffer.from(arrayBuffer[, byteOffset[, length]]) + **/ +Buffer.from = function (value, encodingOrOffset, length) { + return from(value, encodingOrOffset, length) +} + +// Note: Change prototype *after* Buffer.from is defined to workaround Chrome bug: +// https://github.com/feross/buffer/pull/148 +Object.setPrototypeOf(Buffer.prototype, Uint8Array.prototype) +Object.setPrototypeOf(Buffer, Uint8Array) + +function assertSize (size) { + if (typeof size !== 'number') { + throw new TypeError('"size" argument must be of type number') + } else if (size < 0) { + throw new RangeError('The value "' + size + '" is invalid for option "size"') + } +} + +function alloc (size, fill, encoding) { + assertSize(size) + if (size <= 0) { + return createBuffer(size) + } + if (fill !== undefined) { + // Only pay attention to encoding if it's a string. This + // prevents accidentally sending in a number that would + // be interpreted as a start offset. + return typeof encoding === 'string' + ? createBuffer(size).fill(fill, encoding) + : createBuffer(size).fill(fill) + } + return createBuffer(size) +} + +/** + * Creates a new filled Buffer instance. + * alloc(size[, fill[, encoding]]) + **/ +Buffer.alloc = function (size, fill, encoding) { + return alloc(size, fill, encoding) +} + +function allocUnsafe (size) { + assertSize(size) + return createBuffer(size < 0 ? 0 : checked(size) | 0) +} + +/** + * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. + * */ +Buffer.allocUnsafe = function (size) { + return allocUnsafe(size) +} +/** + * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. + */ +Buffer.allocUnsafeSlow = function (size) { + return allocUnsafe(size) +} + +function fromString (string, encoding) { + if (typeof encoding !== 'string' || encoding === '') { + encoding = 'utf8' + } + + if (!Buffer.isEncoding(encoding)) { + throw new TypeError('Unknown encoding: ' + encoding) + } + + const length = byteLength(string, encoding) | 0 + let buf = createBuffer(length) + + const actual = buf.write(string, encoding) + + if (actual !== length) { + // Writing a hex string, for example, that contains invalid characters will + // cause everything after the first invalid character to be ignored. (e.g. + // 'abxxcd' will be treated as 'ab') + buf = buf.slice(0, actual) + } + + return buf +} + +function fromArrayLike (array) { + const length = array.length < 0 ? 0 : checked(array.length) | 0 + const buf = createBuffer(length) + for (let i = 0; i < length; i += 1) { + buf[i] = array[i] & 255 + } + return buf +} + +function fromArrayView (arrayView) { + if (isInstance(arrayView, Uint8Array)) { + const copy = new Uint8Array(arrayView) + return fromArrayBuffer(copy.buffer, copy.byteOffset, copy.byteLength) + } + return fromArrayLike(arrayView) +} + +function fromArrayBuffer (array, byteOffset, length) { + if (byteOffset < 0 || array.byteLength < byteOffset) { + throw new RangeError('"offset" is outside of buffer bounds') + } + + if (array.byteLength < byteOffset + (length || 0)) { + throw new RangeError('"length" is outside of buffer bounds') + } + + let buf + if (byteOffset === undefined && length === undefined) { + buf = new Uint8Array(array) + } else if (length === undefined) { + buf = new Uint8Array(array, byteOffset) + } else { + buf = new Uint8Array(array, byteOffset, length) + } + + // Return an augmented `Uint8Array` instance + Object.setPrototypeOf(buf, Buffer.prototype) + + return buf +} + +function fromObject (obj) { + if (Buffer.isBuffer(obj)) { + const len = checked(obj.length) | 0 + const buf = createBuffer(len) + + if (buf.length === 0) { + return buf + } + + obj.copy(buf, 0, 0, len) + return buf + } + + if (obj.length !== undefined) { + if (typeof obj.length !== 'number' || numberIsNaN(obj.length)) { + return createBuffer(0) + } + return fromArrayLike(obj) + } + + if (obj.type === 'Buffer' && Array.isArray(obj.data)) { + return fromArrayLike(obj.data) + } +} + +function checked (length) { + // Note: cannot use `length < K_MAX_LENGTH` here because that fails when + // length is NaN (which is otherwise coerced to zero.) + if (length >= K_MAX_LENGTH) { + throw new RangeError('Attempt to allocate Buffer larger than maximum ' + + 'size: 0x' + K_MAX_LENGTH.toString(16) + ' bytes') + } + return length | 0 +} + +function SlowBuffer (length) { + if (+length != length) { // eslint-disable-line eqeqeq + length = 0 + } + return Buffer.alloc(+length) +} + +Buffer.isBuffer = function isBuffer (b) { + return b != null && b._isBuffer === true && + b !== Buffer.prototype // so Buffer.isBuffer(Buffer.prototype) will be false +} + +Buffer.compare = function compare (a, b) { + if (isInstance(a, Uint8Array)) a = Buffer.from(a, a.offset, a.byteLength) + if (isInstance(b, Uint8Array)) b = Buffer.from(b, b.offset, b.byteLength) + if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { + throw new TypeError( + 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' + ) + } + + if (a === b) return 0 + + let x = a.length + let y = b.length + + for (let i = 0, len = Math.min(x, y); i < len; ++i) { + if (a[i] !== b[i]) { + x = a[i] + y = b[i] + break + } + } + + if (x < y) return -1 + if (y < x) return 1 + return 0 +} + +Buffer.isEncoding = function isEncoding (encoding) { + switch (String(encoding).toLowerCase()) { + case 'hex': + case 'utf8': + case 'utf-8': + case 'ascii': + case 'latin1': + case 'binary': + case 'base64': + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return true + default: + return false + } +} + +Buffer.concat = function concat (list, length) { + if (!Array.isArray(list)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } + + if (list.length === 0) { + return Buffer.alloc(0) + } + + let i + if (length === undefined) { + length = 0 + for (i = 0; i < list.length; ++i) { + length += list[i].length + } + } + + const buffer = Buffer.allocUnsafe(length) + let pos = 0 + for (i = 0; i < list.length; ++i) { + let buf = list[i] + if (isInstance(buf, Uint8Array)) { + if (pos + buf.length > buffer.length) { + if (!Buffer.isBuffer(buf)) { + buf = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) + } + buf.copy(buffer, pos) + } else { + Uint8Array.prototype.set.call( + buffer, + buf, + pos + ) + } + } else if (!Buffer.isBuffer(buf)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } else { + buf.copy(buffer, pos) + } + pos += buf.length + } + return buffer +} + +function byteLength (string, encoding) { + if (Buffer.isBuffer(string)) { + return string.length + } + if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) { + return string.byteLength + } + if (typeof string !== 'string') { + throw new TypeError( + 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. ' + + 'Received type ' + typeof string + ) + } + + const len = string.length + const mustMatch = (arguments.length > 2 && arguments[2] === true) + if (!mustMatch && len === 0) return 0 + + // Use a for loop to avoid recursion + let loweredCase = false + for (;;) { + switch (encoding) { + case 'ascii': + case 'latin1': + case 'binary': + return len + case 'utf8': + case 'utf-8': + return utf8ToBytes(string).length + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return len * 2 + case 'hex': + return len >>> 1 + case 'base64': + return base64ToBytes(string).length + default: + if (loweredCase) { + return mustMatch ? -1 : utf8ToBytes(string).length // assume utf8 + } + encoding = ('' + encoding).toLowerCase() + loweredCase = true + } + } +} +Buffer.byteLength = byteLength + +function slowToString (encoding, start, end) { + let loweredCase = false + + // No need to verify that "this.length <= MAX_UINT32" since it's a read-only + // property of a typed array. + + // This behaves neither like String nor Uint8Array in that we set start/end + // to their upper/lower bounds if the value passed is out of range. + // undefined is handled specially as per ECMA-262 6th Edition, + // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. + if (start === undefined || start < 0) { + start = 0 + } + // Return early if start > this.length. Done here to prevent potential uint32 + // coercion fail below. + if (start > this.length) { + return '' + } + + if (end === undefined || end > this.length) { + end = this.length + } + + if (end <= 0) { + return '' + } + + // Force coercion to uint32. This will also coerce falsey/NaN values to 0. + end >>>= 0 + start >>>= 0 + + if (end <= start) { + return '' + } + + if (!encoding) encoding = 'utf8' + + while (true) { + switch (encoding) { + case 'hex': + return hexSlice(this, start, end) + + case 'utf8': + case 'utf-8': + return utf8Slice(this, start, end) + + case 'ascii': + return asciiSlice(this, start, end) + + case 'latin1': + case 'binary': + return latin1Slice(this, start, end) + + case 'base64': + return base64Slice(this, start, end) + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return utf16leSlice(this, start, end) + + default: + if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) + encoding = (encoding + '').toLowerCase() + loweredCase = true + } + } +} + +// This property is used by `Buffer.isBuffer` (and the `is-buffer` npm package) +// to detect a Buffer instance. It's not possible to use `instanceof Buffer` +// reliably in a browserify context because there could be multiple different +// copies of the 'buffer' package in use. This method works even for Buffer +// instances that were created from another copy of the `buffer` package. +// See: https://github.com/feross/buffer/issues/154 +Buffer.prototype._isBuffer = true + +function swap (b, n, m) { + const i = b[n] + b[n] = b[m] + b[m] = i +} + +Buffer.prototype.swap16 = function swap16 () { + const len = this.length + if (len % 2 !== 0) { + throw new RangeError('Buffer size must be a multiple of 16-bits') + } + for (let i = 0; i < len; i += 2) { + swap(this, i, i + 1) + } + return this +} + +Buffer.prototype.swap32 = function swap32 () { + const len = this.length + if (len % 4 !== 0) { + throw new RangeError('Buffer size must be a multiple of 32-bits') + } + for (let i = 0; i < len; i += 4) { + swap(this, i, i + 3) + swap(this, i + 1, i + 2) + } + return this +} + +Buffer.prototype.swap64 = function swap64 () { + const len = this.length + if (len % 8 !== 0) { + throw new RangeError('Buffer size must be a multiple of 64-bits') + } + for (let i = 0; i < len; i += 8) { + swap(this, i, i + 7) + swap(this, i + 1, i + 6) + swap(this, i + 2, i + 5) + swap(this, i + 3, i + 4) + } + return this +} + +Buffer.prototype.toString = function toString () { + const length = this.length + if (length === 0) return '' + if (arguments.length === 0) return utf8Slice(this, 0, length) + return slowToString.apply(this, arguments) +} + +Buffer.prototype.toLocaleString = Buffer.prototype.toString + +Buffer.prototype.equals = function equals (b) { + if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer') + if (this === b) return true + return Buffer.compare(this, b) === 0 +} + +Buffer.prototype.inspect = function inspect () { + let str = '' + const max = exports.INSPECT_MAX_BYTES + str = this.toString('hex', 0, max).replace(/(.{2})/g, '$1 ').trim() + if (this.length > max) str += ' ... ' + return '' +} +if (customInspectSymbol) { + Buffer.prototype[customInspectSymbol] = Buffer.prototype.inspect +} + +Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) { + if (isInstance(target, Uint8Array)) { + target = Buffer.from(target, target.offset, target.byteLength) + } + if (!Buffer.isBuffer(target)) { + throw new TypeError( + 'The "target" argument must be one of type Buffer or Uint8Array. ' + + 'Received type ' + (typeof target) + ) + } + + if (start === undefined) { + start = 0 + } + if (end === undefined) { + end = target ? target.length : 0 + } + if (thisStart === undefined) { + thisStart = 0 + } + if (thisEnd === undefined) { + thisEnd = this.length + } + + if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { + throw new RangeError('out of range index') + } + + if (thisStart >= thisEnd && start >= end) { + return 0 + } + if (thisStart >= thisEnd) { + return -1 + } + if (start >= end) { + return 1 + } + + start >>>= 0 + end >>>= 0 + thisStart >>>= 0 + thisEnd >>>= 0 + + if (this === target) return 0 + + let x = thisEnd - thisStart + let y = end - start + const len = Math.min(x, y) + + const thisCopy = this.slice(thisStart, thisEnd) + const targetCopy = target.slice(start, end) + + for (let i = 0; i < len; ++i) { + if (thisCopy[i] !== targetCopy[i]) { + x = thisCopy[i] + y = targetCopy[i] + break + } + } + + if (x < y) return -1 + if (y < x) return 1 + return 0 +} + +// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, +// OR the last index of `val` in `buffer` at offset <= `byteOffset`. +// +// Arguments: +// - buffer - a Buffer to search +// - val - a string, Buffer, or number +// - byteOffset - an index into `buffer`; will be clamped to an int32 +// - encoding - an optional encoding, relevant is val is a string +// - dir - true for indexOf, false for lastIndexOf +function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) { + // Empty buffer means no match + if (buffer.length === 0) return -1 + + // Normalize byteOffset + if (typeof byteOffset === 'string') { + encoding = byteOffset + byteOffset = 0 + } else if (byteOffset > 0x7fffffff) { + byteOffset = 0x7fffffff + } else if (byteOffset < -0x80000000) { + byteOffset = -0x80000000 + } + byteOffset = +byteOffset // Coerce to Number. + if (numberIsNaN(byteOffset)) { + // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer + byteOffset = dir ? 0 : (buffer.length - 1) + } + + // Normalize byteOffset: negative offsets start from the end of the buffer + if (byteOffset < 0) byteOffset = buffer.length + byteOffset + if (byteOffset >= buffer.length) { + if (dir) return -1 + else byteOffset = buffer.length - 1 + } else if (byteOffset < 0) { + if (dir) byteOffset = 0 + else return -1 + } + + // Normalize val + if (typeof val === 'string') { + val = Buffer.from(val, encoding) + } + + // Finally, search either indexOf (if dir is true) or lastIndexOf + if (Buffer.isBuffer(val)) { + // Special case: looking for empty string/buffer always fails + if (val.length === 0) { + return -1 + } + return arrayIndexOf(buffer, val, byteOffset, encoding, dir) + } else if (typeof val === 'number') { + val = val & 0xFF // Search for a byte value [0-255] + if (typeof Uint8Array.prototype.indexOf === 'function') { + if (dir) { + return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset) + } else { + return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset) + } + } + return arrayIndexOf(buffer, [val], byteOffset, encoding, dir) + } + + throw new TypeError('val must be string, number or Buffer') +} + +function arrayIndexOf (arr, val, byteOffset, encoding, dir) { + let indexSize = 1 + let arrLength = arr.length + let valLength = val.length + + if (encoding !== undefined) { + encoding = String(encoding).toLowerCase() + if (encoding === 'ucs2' || encoding === 'ucs-2' || + encoding === 'utf16le' || encoding === 'utf-16le') { + if (arr.length < 2 || val.length < 2) { + return -1 + } + indexSize = 2 + arrLength /= 2 + valLength /= 2 + byteOffset /= 2 + } + } + + function read (buf, i) { + if (indexSize === 1) { + return buf[i] + } else { + return buf.readUInt16BE(i * indexSize) + } + } + + let i + if (dir) { + let foundIndex = -1 + for (i = byteOffset; i < arrLength; i++) { + if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { + if (foundIndex === -1) foundIndex = i + if (i - foundIndex + 1 === valLength) return foundIndex * indexSize + } else { + if (foundIndex !== -1) i -= i - foundIndex + foundIndex = -1 + } + } + } else { + if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength + for (i = byteOffset; i >= 0; i--) { + let found = true + for (let j = 0; j < valLength; j++) { + if (read(arr, i + j) !== read(val, j)) { + found = false + break + } + } + if (found) return i + } + } + + return -1 +} + +Buffer.prototype.includes = function includes (val, byteOffset, encoding) { + return this.indexOf(val, byteOffset, encoding) !== -1 +} + +Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, true) +} + +Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, false) +} + +function hexWrite (buf, string, offset, length) { + offset = Number(offset) || 0 + const remaining = buf.length - offset + if (!length) { + length = remaining + } else { + length = Number(length) + if (length > remaining) { + length = remaining + } + } + + const strLen = string.length + + if (length > strLen / 2) { + length = strLen / 2 + } + let i + for (i = 0; i < length; ++i) { + const parsed = parseInt(string.substr(i * 2, 2), 16) + if (numberIsNaN(parsed)) return i + buf[offset + i] = parsed + } + return i +} + +function utf8Write (buf, string, offset, length) { + return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) +} + +function asciiWrite (buf, string, offset, length) { + return blitBuffer(asciiToBytes(string), buf, offset, length) +} + +function base64Write (buf, string, offset, length) { + return blitBuffer(base64ToBytes(string), buf, offset, length) +} + +function ucs2Write (buf, string, offset, length) { + return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length) +} + +Buffer.prototype.write = function write (string, offset, length, encoding) { + // Buffer#write(string) + if (offset === undefined) { + encoding = 'utf8' + length = this.length + offset = 0 + // Buffer#write(string, encoding) + } else if (length === undefined && typeof offset === 'string') { + encoding = offset + length = this.length + offset = 0 + // Buffer#write(string, offset[, length][, encoding]) + } else if (isFinite(offset)) { + offset = offset >>> 0 + if (isFinite(length)) { + length = length >>> 0 + if (encoding === undefined) encoding = 'utf8' + } else { + encoding = length + length = undefined + } + } else { + throw new Error( + 'Buffer.write(string, encoding, offset[, length]) is no longer supported' + ) + } + + const remaining = this.length - offset + if (length === undefined || length > remaining) length = remaining + + if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { + throw new RangeError('Attempt to write outside buffer bounds') + } + + if (!encoding) encoding = 'utf8' + + let loweredCase = false + for (;;) { + switch (encoding) { + case 'hex': + return hexWrite(this, string, offset, length) + + case 'utf8': + case 'utf-8': + return utf8Write(this, string, offset, length) + + case 'ascii': + case 'latin1': + case 'binary': + return asciiWrite(this, string, offset, length) + + case 'base64': + // Warning: maxLength not taken into account in base64Write + return base64Write(this, string, offset, length) + + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return ucs2Write(this, string, offset, length) + + default: + if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) + encoding = ('' + encoding).toLowerCase() + loweredCase = true + } + } +} + +Buffer.prototype.toJSON = function toJSON () { + return { + type: 'Buffer', + data: Array.prototype.slice.call(this._arr || this, 0) + } +} + +function base64Slice (buf, start, end) { + if (start === 0 && end === buf.length) { + return base64.fromByteArray(buf) + } else { + return base64.fromByteArray(buf.slice(start, end)) + } +} + +function utf8Slice (buf, start, end) { + end = Math.min(buf.length, end) + const res = [] + + let i = start + while (i < end) { + const firstByte = buf[i] + let codePoint = null + let bytesPerSequence = (firstByte > 0xEF) + ? 4 + : (firstByte > 0xDF) + ? 3 + : (firstByte > 0xBF) + ? 2 + : 1 + + if (i + bytesPerSequence <= end) { + let secondByte, thirdByte, fourthByte, tempCodePoint + + switch (bytesPerSequence) { + case 1: + if (firstByte < 0x80) { + codePoint = firstByte + } + break + case 2: + secondByte = buf[i + 1] + if ((secondByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F) + if (tempCodePoint > 0x7F) { + codePoint = tempCodePoint + } + } + break + case 3: + secondByte = buf[i + 1] + thirdByte = buf[i + 2] + if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F) + if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) { + codePoint = tempCodePoint + } + } + break + case 4: + secondByte = buf[i + 1] + thirdByte = buf[i + 2] + fourthByte = buf[i + 3] + if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) { + tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F) + if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) { + codePoint = tempCodePoint + } + } + } + } + + if (codePoint === null) { + // we did not generate a valid codePoint so insert a + // replacement char (U+FFFD) and advance only 1 byte + codePoint = 0xFFFD + bytesPerSequence = 1 + } else if (codePoint > 0xFFFF) { + // encode to utf16 (surrogate pair dance) + codePoint -= 0x10000 + res.push(codePoint >>> 10 & 0x3FF | 0xD800) + codePoint = 0xDC00 | codePoint & 0x3FF + } + + res.push(codePoint) + i += bytesPerSequence + } + + return decodeCodePointsArray(res) +} + +// Based on http://stackoverflow.com/a/22747272/680742, the browser with +// the lowest limit is Chrome, with 0x10000 args. +// We go 1 magnitude less, for safety +const MAX_ARGUMENTS_LENGTH = 0x1000 + +function decodeCodePointsArray (codePoints) { + const len = codePoints.length + if (len <= MAX_ARGUMENTS_LENGTH) { + return String.fromCharCode.apply(String, codePoints) // avoid extra slice() + } + + // Decode in chunks to avoid "call stack size exceeded". + let res = '' + let i = 0 + while (i < len) { + res += String.fromCharCode.apply( + String, + codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) + ) + } + return res +} + +function asciiSlice (buf, start, end) { + let ret = '' + end = Math.min(buf.length, end) + + for (let i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i] & 0x7F) + } + return ret +} + +function latin1Slice (buf, start, end) { + let ret = '' + end = Math.min(buf.length, end) + + for (let i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i]) + } + return ret +} + +function hexSlice (buf, start, end) { + const len = buf.length + + if (!start || start < 0) start = 0 + if (!end || end < 0 || end > len) end = len + + let out = '' + for (let i = start; i < end; ++i) { + out += hexSliceLookupTable[buf[i]] + } + return out +} + +function utf16leSlice (buf, start, end) { + const bytes = buf.slice(start, end) + let res = '' + // If bytes.length is odd, the last 8 bits must be ignored (same as node.js) + for (let i = 0; i < bytes.length - 1; i += 2) { + res += String.fromCharCode(bytes[i] + (bytes[i + 1] * 256)) + } + return res +} + +Buffer.prototype.slice = function slice (start, end) { + const len = this.length + start = ~~start + end = end === undefined ? len : ~~end + + if (start < 0) { + start += len + if (start < 0) start = 0 + } else if (start > len) { + start = len + } + + if (end < 0) { + end += len + if (end < 0) end = 0 + } else if (end > len) { + end = len + } + + if (end < start) end = start + + const newBuf = this.subarray(start, end) + // Return an augmented `Uint8Array` instance + Object.setPrototypeOf(newBuf, Buffer.prototype) + + return newBuf +} + +/* + * Need to make sure that buffer isn't trying to write out of bounds. + */ +function checkOffset (offset, ext, length) { + if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint') + if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length') +} + +Buffer.prototype.readUintLE = +Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) checkOffset(offset, byteLength, this.length) + + let val = this[offset] + let mul = 1 + let i = 0 + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul + } + + return val +} + +Buffer.prototype.readUintBE = +Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) { + checkOffset(offset, byteLength, this.length) + } + + let val = this[offset + --byteLength] + let mul = 1 + while (byteLength > 0 && (mul *= 0x100)) { + val += this[offset + --byteLength] * mul + } + + return val +} + +Buffer.prototype.readUint8 = +Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 1, this.length) + return this[offset] +} + +Buffer.prototype.readUint16LE = +Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + return this[offset] | (this[offset + 1] << 8) +} + +Buffer.prototype.readUint16BE = +Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + return (this[offset] << 8) | this[offset + 1] +} + +Buffer.prototype.readUint32LE = +Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return ((this[offset]) | + (this[offset + 1] << 8) | + (this[offset + 2] << 16)) + + (this[offset + 3] * 0x1000000) +} + +Buffer.prototype.readUint32BE = +Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return (this[offset] * 0x1000000) + + ((this[offset + 1] << 16) | + (this[offset + 2] << 8) | + this[offset + 3]) +} + +Buffer.prototype.readBigUInt64LE = defineBigIntMethod(function readBigUInt64LE (offset) { + offset = offset >>> 0 + validateNumber(offset, 'offset') + const first = this[offset] + const last = this[offset + 7] + if (first === undefined || last === undefined) { + boundsError(offset, this.length - 8) + } + + const lo = first + + this[++offset] * 2 ** 8 + + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 24 + + const hi = this[++offset] + + this[++offset] * 2 ** 8 + + this[++offset] * 2 ** 16 + + last * 2 ** 24 + + return BigInt(lo) + (BigInt(hi) << BigInt(32)) +}) + +Buffer.prototype.readBigUInt64BE = defineBigIntMethod(function readBigUInt64BE (offset) { + offset = offset >>> 0 + validateNumber(offset, 'offset') + const first = this[offset] + const last = this[offset + 7] + if (first === undefined || last === undefined) { + boundsError(offset, this.length - 8) + } + + const hi = first * 2 ** 24 + + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + + this[++offset] + + const lo = this[++offset] * 2 ** 24 + + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + + last + + return (BigInt(hi) << BigInt(32)) + BigInt(lo) +}) + +Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) checkOffset(offset, byteLength, this.length) + + let val = this[offset] + let mul = 1 + let i = 0 + while (++i < byteLength && (mul *= 0x100)) { + val += this[offset + i] * mul + } + mul *= 0x80 + + if (val >= mul) val -= Math.pow(2, 8 * byteLength) + + return val +} + +Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) { + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) checkOffset(offset, byteLength, this.length) + + let i = byteLength + let mul = 1 + let val = this[offset + --i] + while (i > 0 && (mul *= 0x100)) { + val += this[offset + --i] * mul + } + mul *= 0x80 + + if (val >= mul) val -= Math.pow(2, 8 * byteLength) + + return val +} + +Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 1, this.length) + if (!(this[offset] & 0x80)) return (this[offset]) + return ((0xff - this[offset] + 1) * -1) +} + +Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + const val = this[offset] | (this[offset + 1] << 8) + return (val & 0x8000) ? val | 0xFFFF0000 : val +} + +Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 2, this.length) + const val = this[offset + 1] | (this[offset] << 8) + return (val & 0x8000) ? val | 0xFFFF0000 : val +} + +Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return (this[offset]) | + (this[offset + 1] << 8) | + (this[offset + 2] << 16) | + (this[offset + 3] << 24) +} + +Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + + return (this[offset] << 24) | + (this[offset + 1] << 16) | + (this[offset + 2] << 8) | + (this[offset + 3]) +} + +Buffer.prototype.readBigInt64LE = defineBigIntMethod(function readBigInt64LE (offset) { + offset = offset >>> 0 + validateNumber(offset, 'offset') + const first = this[offset] + const last = this[offset + 7] + if (first === undefined || last === undefined) { + boundsError(offset, this.length - 8) + } + + const val = this[offset + 4] + + this[offset + 5] * 2 ** 8 + + this[offset + 6] * 2 ** 16 + + (last << 24) // Overflow + + return (BigInt(val) << BigInt(32)) + + BigInt(first + + this[++offset] * 2 ** 8 + + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 24) +}) + +Buffer.prototype.readBigInt64BE = defineBigIntMethod(function readBigInt64BE (offset) { + offset = offset >>> 0 + validateNumber(offset, 'offset') + const first = this[offset] + const last = this[offset + 7] + if (first === undefined || last === undefined) { + boundsError(offset, this.length - 8) + } + + const val = (first << 24) + // Overflow + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + + this[++offset] + + return (BigInt(val) << BigInt(32)) + + BigInt(this[++offset] * 2 ** 24 + + this[++offset] * 2 ** 16 + + this[++offset] * 2 ** 8 + + last) +}) + +Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + return ieee754.read(this, offset, true, 23, 4) +} + +Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 4, this.length) + return ieee754.read(this, offset, false, 23, 4) +} + +Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 8, this.length) + return ieee754.read(this, offset, true, 52, 8) +} + +Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) { + offset = offset >>> 0 + if (!noAssert) checkOffset(offset, 8, this.length) + return ieee754.read(this, offset, false, 52, 8) +} + +function checkInt (buf, value, offset, ext, max, min) { + if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance') + if (value > max || value < min) throw new RangeError('"value" argument is out of bounds') + if (offset + ext > buf.length) throw new RangeError('Index out of range') +} + +Buffer.prototype.writeUintLE = +Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) { + const maxBytes = Math.pow(2, 8 * byteLength) - 1 + checkInt(this, value, offset, byteLength, maxBytes, 0) + } + + let mul = 1 + let i = 0 + this[offset] = value & 0xFF + while (++i < byteLength && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeUintBE = +Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + byteLength = byteLength >>> 0 + if (!noAssert) { + const maxBytes = Math.pow(2, 8 * byteLength) - 1 + checkInt(this, value, offset, byteLength, maxBytes, 0) + } + + let i = byteLength - 1 + let mul = 1 + this[offset + i] = value & 0xFF + while (--i >= 0 && (mul *= 0x100)) { + this[offset + i] = (value / mul) & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeUint8 = +Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0) + this[offset] = (value & 0xff) + return offset + 1 +} + +Buffer.prototype.writeUint16LE = +Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) + this[offset] = (value & 0xff) + this[offset + 1] = (value >>> 8) + return offset + 2 +} + +Buffer.prototype.writeUint16BE = +Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) + this[offset] = (value >>> 8) + this[offset + 1] = (value & 0xff) + return offset + 2 +} + +Buffer.prototype.writeUint32LE = +Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) + this[offset + 3] = (value >>> 24) + this[offset + 2] = (value >>> 16) + this[offset + 1] = (value >>> 8) + this[offset] = (value & 0xff) + return offset + 4 +} + +Buffer.prototype.writeUint32BE = +Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) + this[offset] = (value >>> 24) + this[offset + 1] = (value >>> 16) + this[offset + 2] = (value >>> 8) + this[offset + 3] = (value & 0xff) + return offset + 4 +} + +function wrtBigUInt64LE (buf, value, offset, min, max) { + checkIntBI(value, min, max, buf, offset, 7) + + let lo = Number(value & BigInt(0xffffffff)) + buf[offset++] = lo + lo = lo >> 8 + buf[offset++] = lo + lo = lo >> 8 + buf[offset++] = lo + lo = lo >> 8 + buf[offset++] = lo + let hi = Number(value >> BigInt(32) & BigInt(0xffffffff)) + buf[offset++] = hi + hi = hi >> 8 + buf[offset++] = hi + hi = hi >> 8 + buf[offset++] = hi + hi = hi >> 8 + buf[offset++] = hi + return offset +} + +function wrtBigUInt64BE (buf, value, offset, min, max) { + checkIntBI(value, min, max, buf, offset, 7) + + let lo = Number(value & BigInt(0xffffffff)) + buf[offset + 7] = lo + lo = lo >> 8 + buf[offset + 6] = lo + lo = lo >> 8 + buf[offset + 5] = lo + lo = lo >> 8 + buf[offset + 4] = lo + let hi = Number(value >> BigInt(32) & BigInt(0xffffffff)) + buf[offset + 3] = hi + hi = hi >> 8 + buf[offset + 2] = hi + hi = hi >> 8 + buf[offset + 1] = hi + hi = hi >> 8 + buf[offset] = hi + return offset + 8 +} + +Buffer.prototype.writeBigUInt64LE = defineBigIntMethod(function writeBigUInt64LE (value, offset = 0) { + return wrtBigUInt64LE(this, value, offset, BigInt(0), BigInt('0xffffffffffffffff')) +}) + +Buffer.prototype.writeBigUInt64BE = defineBigIntMethod(function writeBigUInt64BE (value, offset = 0) { + return wrtBigUInt64BE(this, value, offset, BigInt(0), BigInt('0xffffffffffffffff')) +}) + +Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + const limit = Math.pow(2, (8 * byteLength) - 1) + + checkInt(this, value, offset, byteLength, limit - 1, -limit) + } + + let i = 0 + let mul = 1 + let sub = 0 + this[offset] = value & 0xFF + while (++i < byteLength && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { + sub = 1 + } + this[offset + i] = ((value / mul) >> 0) - sub & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + const limit = Math.pow(2, (8 * byteLength) - 1) + + checkInt(this, value, offset, byteLength, limit - 1, -limit) + } + + let i = byteLength - 1 + let mul = 1 + let sub = 0 + this[offset + i] = value & 0xFF + while (--i >= 0 && (mul *= 0x100)) { + if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { + sub = 1 + } + this[offset + i] = ((value / mul) >> 0) - sub & 0xFF + } + + return offset + byteLength +} + +Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80) + if (value < 0) value = 0xff + value + 1 + this[offset] = (value & 0xff) + return offset + 1 +} + +Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) + this[offset] = (value & 0xff) + this[offset + 1] = (value >>> 8) + return offset + 2 +} + +Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) + this[offset] = (value >>> 8) + this[offset + 1] = (value & 0xff) + return offset + 2 +} + +Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) + this[offset] = (value & 0xff) + this[offset + 1] = (value >>> 8) + this[offset + 2] = (value >>> 16) + this[offset + 3] = (value >>> 24) + return offset + 4 +} + +Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) + if (value < 0) value = 0xffffffff + value + 1 + this[offset] = (value >>> 24) + this[offset + 1] = (value >>> 16) + this[offset + 2] = (value >>> 8) + this[offset + 3] = (value & 0xff) + return offset + 4 +} + +Buffer.prototype.writeBigInt64LE = defineBigIntMethod(function writeBigInt64LE (value, offset = 0) { + return wrtBigUInt64LE(this, value, offset, -BigInt('0x8000000000000000'), BigInt('0x7fffffffffffffff')) +}) + +Buffer.prototype.writeBigInt64BE = defineBigIntMethod(function writeBigInt64BE (value, offset = 0) { + return wrtBigUInt64BE(this, value, offset, -BigInt('0x8000000000000000'), BigInt('0x7fffffffffffffff')) +}) + +function checkIEEE754 (buf, value, offset, ext, max, min) { + if (offset + ext > buf.length) throw new RangeError('Index out of range') + if (offset < 0) throw new RangeError('Index out of range') +} + +function writeFloat (buf, value, offset, littleEndian, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38) + } + ieee754.write(buf, value, offset, littleEndian, 23, 4) + return offset + 4 +} + +Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) { + return writeFloat(this, value, offset, true, noAssert) +} + +Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) { + return writeFloat(this, value, offset, false, noAssert) +} + +function writeDouble (buf, value, offset, littleEndian, noAssert) { + value = +value + offset = offset >>> 0 + if (!noAssert) { + checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308) + } + ieee754.write(buf, value, offset, littleEndian, 52, 8) + return offset + 8 +} + +Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) { + return writeDouble(this, value, offset, true, noAssert) +} + +Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) { + return writeDouble(this, value, offset, false, noAssert) +} + +// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) +Buffer.prototype.copy = function copy (target, targetStart, start, end) { + if (!Buffer.isBuffer(target)) throw new TypeError('argument should be a Buffer') + if (!start) start = 0 + if (!end && end !== 0) end = this.length + if (targetStart >= target.length) targetStart = target.length + if (!targetStart) targetStart = 0 + if (end > 0 && end < start) end = start + + // Copy 0 bytes; we're done + if (end === start) return 0 + if (target.length === 0 || this.length === 0) return 0 + + // Fatal error conditions + if (targetStart < 0) { + throw new RangeError('targetStart out of bounds') + } + if (start < 0 || start >= this.length) throw new RangeError('Index out of range') + if (end < 0) throw new RangeError('sourceEnd out of bounds') + + // Are we oob? + if (end > this.length) end = this.length + if (target.length - targetStart < end - start) { + end = target.length - targetStart + start + } + + const len = end - start + + if (this === target && typeof Uint8Array.prototype.copyWithin === 'function') { + // Use built-in when available, missing from IE11 + this.copyWithin(targetStart, start, end) + } else { + Uint8Array.prototype.set.call( + target, + this.subarray(start, end), + targetStart + ) + } + + return len +} + +// Usage: +// buffer.fill(number[, offset[, end]]) +// buffer.fill(buffer[, offset[, end]]) +// buffer.fill(string[, offset[, end]][, encoding]) +Buffer.prototype.fill = function fill (val, start, end, encoding) { + // Handle string cases: + if (typeof val === 'string') { + if (typeof start === 'string') { + encoding = start + start = 0 + end = this.length + } else if (typeof end === 'string') { + encoding = end + end = this.length + } + if (encoding !== undefined && typeof encoding !== 'string') { + throw new TypeError('encoding must be a string') + } + if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) { + throw new TypeError('Unknown encoding: ' + encoding) + } + if (val.length === 1) { + const code = val.charCodeAt(0) + if ((encoding === 'utf8' && code < 128) || + encoding === 'latin1') { + // Fast path: If `val` fits into a single byte, use that numeric value. + val = code + } + } + } else if (typeof val === 'number') { + val = val & 255 + } else if (typeof val === 'boolean') { + val = Number(val) + } + + // Invalid ranges are not set to a default, so can range check early. + if (start < 0 || this.length < start || this.length < end) { + throw new RangeError('Out of range index') + } + + if (end <= start) { + return this + } + + start = start >>> 0 + end = end === undefined ? this.length : end >>> 0 + + if (!val) val = 0 + + let i + if (typeof val === 'number') { + for (i = start; i < end; ++i) { + this[i] = val + } + } else { + const bytes = Buffer.isBuffer(val) + ? val + : Buffer.from(val, encoding) + const len = bytes.length + if (len === 0) { + throw new TypeError('The value "' + val + + '" is invalid for argument "value"') + } + for (i = 0; i < end - start; ++i) { + this[i + start] = bytes[i % len] + } + } + + return this +} + +// CUSTOM ERRORS +// ============= + +// Simplified versions from Node, changed for Buffer-only usage +const errors = {} +function E (sym, getMessage, Base) { + errors[sym] = class NodeError extends Base { + constructor () { + super() + + Object.defineProperty(this, 'message', { + value: getMessage.apply(this, arguments), + writable: true, + configurable: true + }) + + // Add the error code to the name to include it in the stack trace. + this.name = `${this.name} [${sym}]` + // Access the stack to generate the error message including the error code + // from the name. + this.stack // eslint-disable-line no-unused-expressions + // Reset the name to the actual name. + delete this.name + } + + get code () { + return sym + } + + set code (value) { + Object.defineProperty(this, 'code', { + configurable: true, + enumerable: true, + value, + writable: true + }) + } + + toString () { + return `${this.name} [${sym}]: ${this.message}` + } + } +} + +E('ERR_BUFFER_OUT_OF_BOUNDS', + function (name) { + if (name) { + return `${name} is outside of buffer bounds` + } + + return 'Attempt to access memory outside buffer bounds' + }, RangeError) +E('ERR_INVALID_ARG_TYPE', + function (name, actual) { + return `The "${name}" argument must be of type number. Received type ${typeof actual}` + }, TypeError) +E('ERR_OUT_OF_RANGE', + function (str, range, input) { + let msg = `The value of "${str}" is out of range.` + let received = input + if (Number.isInteger(input) && Math.abs(input) > 2 ** 32) { + received = addNumericalSeparator(String(input)) + } else if (typeof input === 'bigint') { + received = String(input) + if (input > BigInt(2) ** BigInt(32) || input < -(BigInt(2) ** BigInt(32))) { + received = addNumericalSeparator(received) + } + received += 'n' + } + msg += ` It must be ${range}. Received ${received}` + return msg + }, RangeError) + +function addNumericalSeparator (val) { + let res = '' + let i = val.length + const start = val[0] === '-' ? 1 : 0 + for (; i >= start + 4; i -= 3) { + res = `_${val.slice(i - 3, i)}${res}` + } + return `${val.slice(0, i)}${res}` +} + +// CHECK FUNCTIONS +// =============== + +function checkBounds (buf, offset, byteLength) { + validateNumber(offset, 'offset') + if (buf[offset] === undefined || buf[offset + byteLength] === undefined) { + boundsError(offset, buf.length - (byteLength + 1)) + } +} + +function checkIntBI (value, min, max, buf, offset, byteLength) { + if (value > max || value < min) { + const n = typeof min === 'bigint' ? 'n' : '' + let range + if (byteLength > 3) { + if (min === 0 || min === BigInt(0)) { + range = `>= 0${n} and < 2${n} ** ${(byteLength + 1) * 8}${n}` + } else { + range = `>= -(2${n} ** ${(byteLength + 1) * 8 - 1}${n}) and < 2 ** ` + + `${(byteLength + 1) * 8 - 1}${n}` + } + } else { + range = `>= ${min}${n} and <= ${max}${n}` + } + throw new errors.ERR_OUT_OF_RANGE('value', range, value) + } + checkBounds(buf, offset, byteLength) +} + +function validateNumber (value, name) { + if (typeof value !== 'number') { + throw new errors.ERR_INVALID_ARG_TYPE(name, 'number', value) + } +} + +function boundsError (value, length, type) { + if (Math.floor(value) !== value) { + validateNumber(value, type) + throw new errors.ERR_OUT_OF_RANGE(type || 'offset', 'an integer', value) + } + + if (length < 0) { + throw new errors.ERR_BUFFER_OUT_OF_BOUNDS() + } + + throw new errors.ERR_OUT_OF_RANGE(type || 'offset', + `>= ${type ? 1 : 0} and <= ${length}`, + value) +} + +// HELPER FUNCTIONS +// ================ + +const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g + +function base64clean (str) { + // Node takes equal signs as end of the Base64 encoding + str = str.split('=')[0] + // Node strips out invalid characters like \n and \t from the string, base64-js does not + str = str.trim().replace(INVALID_BASE64_RE, '') + // Node converts strings with length < 2 to '' + if (str.length < 2) return '' + // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not + while (str.length % 4 !== 0) { + str = str + '=' + } + return str +} + +function utf8ToBytes (string, units) { + units = units || Infinity + let codePoint + const length = string.length + let leadSurrogate = null + const bytes = [] + + for (let i = 0; i < length; ++i) { + codePoint = string.charCodeAt(i) + + // is surrogate component + if (codePoint > 0xD7FF && codePoint < 0xE000) { + // last char was a lead + if (!leadSurrogate) { + // no lead yet + if (codePoint > 0xDBFF) { + // unexpected trail + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + continue + } else if (i + 1 === length) { + // unpaired lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + continue + } + + // valid lead + leadSurrogate = codePoint + + continue + } + + // 2 leads in a row + if (codePoint < 0xDC00) { + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + leadSurrogate = codePoint + continue + } + + // valid surrogate pair + codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000 + } else if (leadSurrogate) { + // valid bmp char, but last char was a lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + } + + leadSurrogate = null + + // encode utf8 + if (codePoint < 0x80) { + if ((units -= 1) < 0) break + bytes.push(codePoint) + } else if (codePoint < 0x800) { + if ((units -= 2) < 0) break + bytes.push( + codePoint >> 0x6 | 0xC0, + codePoint & 0x3F | 0x80 + ) + } else if (codePoint < 0x10000) { + if ((units -= 3) < 0) break + bytes.push( + codePoint >> 0xC | 0xE0, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ) + } else if (codePoint < 0x110000) { + if ((units -= 4) < 0) break + bytes.push( + codePoint >> 0x12 | 0xF0, + codePoint >> 0xC & 0x3F | 0x80, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ) + } else { + throw new Error('Invalid code point') + } + } + + return bytes +} + +function asciiToBytes (str) { + const byteArray = [] + for (let i = 0; i < str.length; ++i) { + // Node's code seems to be doing this and not & 0x7F.. + byteArray.push(str.charCodeAt(i) & 0xFF) + } + return byteArray +} + +function utf16leToBytes (str, units) { + let c, hi, lo + const byteArray = [] + for (let i = 0; i < str.length; ++i) { + if ((units -= 2) < 0) break + + c = str.charCodeAt(i) + hi = c >> 8 + lo = c % 256 + byteArray.push(lo) + byteArray.push(hi) + } + + return byteArray +} + +function base64ToBytes (str) { + return base64.toByteArray(base64clean(str)) +} + +function blitBuffer (src, dst, offset, length) { + let i + for (i = 0; i < length; ++i) { + if ((i + offset >= dst.length) || (i >= src.length)) break + dst[i + offset] = src[i] + } + return i +} + +// ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass +// the `instanceof` check but they should be treated as of that type. +// See: https://github.com/feross/buffer/issues/166 +function isInstance (obj, type) { + return obj instanceof type || + (obj != null && obj.constructor != null && obj.constructor.name != null && + obj.constructor.name === type.name) +} +function numberIsNaN (obj) { + // For IE11 support + return obj !== obj // eslint-disable-line no-self-compare +} + +// Create lookup table for `toString('hex')` +// See: https://github.com/feross/buffer/issues/219 +const hexSliceLookupTable = (function () { + const alphabet = '0123456789abcdef' + const table = new Array(256) + for (let i = 0; i < 16; ++i) { + const i16 = i * 16 + for (let j = 0; j < 16; ++j) { + table[i16 + j] = alphabet[i] + alphabet[j] + } + } + return table +})() + +// Return not function with Error if BigInt not supported +function defineBigIntMethod (fn) { + return typeof BigInt === 'undefined' ? BufferBigIntNotDefined : fn +} + +function BufferBigIntNotDefined () { + throw new Error('BigInt not supported') +} +"#; diff --git a/test-runner/src/js/vendor_ieee754.rs b/test-runner/src/js/vendor_ieee754.rs new file mode 100644 index 0000000..9441793 --- /dev/null +++ b/test-runner/src/js/vendor_ieee754.rs @@ -0,0 +1,110 @@ +// Read/write IEEE754 floating point numbers from/to a Buffer or array-like object. +// +// License: BSD (3 clause) +// Source: https://github.com/feross/ieee754 + +pub const SCRIPT: &str = r#" +/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ +export function read(buffer, offset, isLE, mLen, nBytes) { + let e, m + const eLen = (nBytes * 8) - mLen - 1 + const eMax = (1 << eLen) - 1 + const eBias = eMax >> 1 + let nBits = -7 + let i = isLE ? (nBytes - 1) : 0 + const d = isLE ? -1 : 1 + let s = buffer[offset + i] + + i += d + + e = s & ((1 << (-nBits)) - 1) + s >>= (-nBits) + nBits += eLen + while (nBits > 0) { + e = (e * 256) + buffer[offset + i] + i += d + nBits -= 8 + } + + m = e & ((1 << (-nBits)) - 1) + e >>= (-nBits) + nBits += mLen + while (nBits > 0) { + m = (m * 256) + buffer[offset + i] + i += d + nBits -= 8 + } + + if (e === 0) { + e = 1 - eBias + } else if (e === eMax) { + return m ? NaN : ((s ? -1 : 1) * Infinity) + } else { + m = m + Math.pow(2, mLen) + e = e - eBias + } + return (s ? -1 : 1) * m * Math.pow(2, e - mLen) +} + +export function write(buffer, value, offset, isLE, mLen, nBytes) { + let e, m, c + let eLen = (nBytes * 8) - mLen - 1 + const eMax = (1 << eLen) - 1 + const eBias = eMax >> 1 + const rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) + let i = isLE ? 0 : (nBytes - 1) + const d = isLE ? 1 : -1 + const s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 + + value = Math.abs(value) + + if (isNaN(value) || value === Infinity) { + m = isNaN(value) ? 1 : 0 + e = eMax + } else { + e = Math.floor(Math.log(value) / Math.LN2) + if (value * (c = Math.pow(2, -e)) < 1) { + e-- + c *= 2 + } + if (e + eBias >= 1) { + value += rt / c + } else { + value += rt * Math.pow(2, 1 - eBias) + } + if (value * c >= 2) { + e++ + c /= 2 + } + + if (e + eBias >= eMax) { + m = 0 + e = eMax + } else if (e + eBias >= 1) { + m = ((value * c) - 1) * Math.pow(2, mLen) + e = e + eBias + } else { + m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) + e = 0 + } + } + + while (mLen >= 8) { + buffer[offset + i] = m & 0xff + i += d + m /= 256 + mLen -= 8 + } + + e = (e << mLen) | m + eLen += mLen + while (eLen > 0) { + buffer[offset + i] = e & 0xff + i += d + e /= 256 + eLen -= 8 + } + + buffer[offset + i - d] |= s * 128 +} +"#; diff --git a/test-runner/src/main.rs b/test-runner/src/main.rs new file mode 100644 index 0000000..106b20e --- /dev/null +++ b/test-runner/src/main.rs @@ -0,0 +1,108 @@ +use std::fs; +use std::path::Path; + +use serde_valid::Validate; + +mod js; +mod structs; + +fn main() { + println!("Parsing vendors"); + + let vendors = fs::read_dir("../vendors").expect("Read vendors directory"); + for vendor in vendors { + let vendor = vendor.expect("Read vendor entry").path(); + if vendor.is_dir() { + parse_vendor(&vendor); + } + } +} + +fn parse_vendor(dir: &Path) { + println!("> Vendor file: {:?}", dir.join("vendor.toml")); + + let vendor_conf: structs::VendorConfiguration = + toml::from_str(&fs::read_to_string(dir.join("vendor.toml")).expect("Read vendor.toml")) + .expect("Parse TOML"); + + println!("> Vendor: {}", vendor_conf.vendor.name); + + println!("> Parsing vendor devices"); + + for device in &vendor_conf.vendor.devices { + parse_device(dir, device); + } +} + +fn parse_device(vendor_dir: &Path, device_config: &str) { + let device_path = vendor_dir.join("devices").join(device_config); + println!(" > Device file: {:?}", device_path); + + let device_config: structs::DeviceConfiguration = + toml::from_str(&fs::read_to_string(device_path).expect("Read device file")) + .expect("Parse TOML"); + + println!(" > Device: {}", device_config.device.name); + + for firmware in &device_config.device.firmware { + println!(" > Firmware: {}", firmware.version); + if let Some(codec) = &firmware.codec { + test_codec(vendor_dir, codec); + } else { + println!(" > Warning: no codec found!"); + } + println!(" > Profiles:"); + for profile in &firmware.profiles { + parse_profile(vendor_dir, profile); + } + } +} + +fn parse_profile(vendor_dir: &Path, profile_config: &str) { + let profile_path = vendor_dir.join("profiles").join(profile_config); + println!(" > Profile file: {:?}", profile_path); + + let profile_conf: structs::ProfileConfiguration = + toml::from_str(&fs::read_to_string(profile_path).expect("Read profile file")) + .expect("Parse TOML"); + + profile_conf.validate().unwrap(); + + println!( + " > Profile: {} - {} - {}", + profile_conf.profile.region, + profile_conf.profile.mac_version, + profile_conf.profile.reg_params_revision + ); +} + +fn test_codec(vendor_dir: &Path, codec: &str) { + let codec_path = vendor_dir.join("codecs").join(codec); + let codec_decode_test_path = vendor_dir + .join("codecs") + .join(format!("test_decode_{}on", codec)); + let codec_encode_test_path = vendor_dir + .join("codecs") + .join(format!("test_encode_{}on", codec)); + + println!(" > Codec file: {:?}", codec_path); + println!( + " > Codecs test encode file: {:?}", + codec_encode_test_path + ); + js::run_tests( + "decodeUplink", + &fs::read_to_string(&codec_path).expect("Read codec script"), + &fs::read_to_string(&codec_decode_test_path).expect("Read codec tests script"), + ); + + println!( + " > Codecs test decode file: {:?}", + codec_decode_test_path + ); + js::run_tests( + "encodeDownlink", + &fs::read_to_string(&codec_path).expect("Read codec script"), + &fs::read_to_string(&codec_encode_test_path).expect("Read codec tests script"), + ); +} diff --git a/test-runner/src/structs.rs b/test-runner/src/structs.rs new file mode 100644 index 0000000..34e6c7d --- /dev/null +++ b/test-runner/src/structs.rs @@ -0,0 +1,80 @@ +use serde::Deserialize; +use serde_valid::Validate; + +#[derive(Deserialize)] +pub struct VendorConfiguration { + pub vendor: Vendor, +} + +#[derive(Deserialize)] +pub struct Vendor { + pub name: String, + pub id: usize, + pub ouis: Vec, + pub devices: Vec, +} + +#[derive(Deserialize)] +pub struct DeviceConfiguration { + pub device: Device, +} + +#[derive(Deserialize)] +pub struct Device { + pub name: String, + pub description: String, + pub firmware: Vec, +} + +#[derive(Deserialize)] +pub struct DeviceFirmware { + pub version: String, + pub profiles: Vec, + pub codec: Option, +} + +#[derive(Deserialize, Validate)] +pub struct ProfileConfiguration { + #[validate] + pub profile: Profile, +} + +#[derive(Default, Deserialize, Validate)] +#[serde(default)] +pub struct Profile { + #[validate(enumerate = ["EU868"])] + pub region: String, + #[validate(enumerate = ["1.0.4"])] + pub mac_version: String, + #[validate(enumerate = ["RP002-1.0.3"])] + pub reg_params_revision: String, + pub supports_otaa: bool, + pub supports_class_b: bool, + pub supports_class_c: bool, + pub max_eirp: usize, + + pub abp: ProfileAbp, + pub class_b: ProfileClassB, + pub class_c: ProfileClassC, +} + +#[derive(Default, Deserialize)] +pub struct ProfileAbp { + pub rx1_delay: usize, + pub rx1_dr_offset: usize, + pub rx2_dr: usize, + pub rx2_freq: usize, +} + +#[derive(Default, Deserialize)] +pub struct ProfileClassB { + pub timeout_secs: usize, + pub ping_slot_nb_k: usize, + pub ping_slot_dr: usize, + pub ping_slot_freq: usize, +} + +#[derive(Default, Deserialize)] +pub struct ProfileClassC { + pub timeout_secs: usize, +} diff --git a/vendors/example-template/codecs/example.js b/vendors/example-template/codecs/example.js new file mode 100644 index 0000000..9e7822c --- /dev/null +++ b/vendors/example-template/codecs/example.js @@ -0,0 +1,13 @@ +function decodeUplink(input) { + return { + data: { + temp: input.bytes[0], + } + }; +} + +function encodeDownlink(input) { + return { + bytes: [input.data.temp] + }; +} diff --git a/vendors/example-template/codecs/test_decode_example.json b/vendors/example-template/codecs/test_decode_example.json new file mode 100644 index 0000000..7cc8cbd --- /dev/null +++ b/vendors/example-template/codecs/test_decode_example.json @@ -0,0 +1,13 @@ +[ + { + "name": "Test decode temperature", + "input": { + "bytes": [21] + }, + "expected": { + "data": { + "temp": 21 + } + } + } +] diff --git a/vendors/example-template/codecs/test_encode_example.json b/vendors/example-template/codecs/test_encode_example.json new file mode 100644 index 0000000..158448b --- /dev/null +++ b/vendors/example-template/codecs/test_encode_example.json @@ -0,0 +1,13 @@ +[ + { + "name": "Test encode temperature", + "input": { + "data": { + "temp": 22 + } + }, + "expected": { + "bytes": [22] + } + } +] diff --git a/vendors/example-template/devices/example.toml b/vendors/example-template/devices/example.toml new file mode 100644 index 0000000..8bf2698 --- /dev/null +++ b/vendors/example-template/devices/example.toml @@ -0,0 +1,8 @@ +[device] + name = "Example device" + description = "This is an example device" + + [[device.firmware]] + version = "1.0.0" + profiles = ["example-EU868.toml"] + codec = "example.js" diff --git a/vendors/example-template/profiles/example-EU868.toml b/vendors/example-template/profiles/example-EU868.toml new file mode 100644 index 0000000..d7527d6 --- /dev/null +++ b/vendors/example-template/profiles/example-EU868.toml @@ -0,0 +1,23 @@ +[profile] + region = "EU868" + mac_version = "1.0.4" + reg_params_revision = "RP002-1.0.3" + supports_otaa = true + supports_class_b = false + supports_class_c = false + max_eirp = 16 + + [profile.abp] + rx1_delay = 1 + rx1_dr_offset = 0 + rx2_dr = 0 + rx2_freq = 869525000 + + [profile.class_b] + timeout_secs = 120 + ping_slot_nb_k = 0 + ping_slot_dr = 0 + ping_slot_freq = 869525000 + + [profile.class_c] + timeout_secs = 120 diff --git a/vendors/example-template/vendor.toml b/vendors/example-template/vendor.toml new file mode 100644 index 0000000..a783e2a --- /dev/null +++ b/vendors/example-template/vendor.toml @@ -0,0 +1,5 @@ +[vendor] + name = "Example vendor" + id = 0 + ouis = [] + devices = ["example.toml"] From 7a88be24e2741ffcb5db062462bb779da92cfa20 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 30 Aug 2024 10:30:10 +0100 Subject: [PATCH 2/7] Rename example-template > example-vendor. --- vendors/{example-template => example-vendor}/codecs/example.js | 0 .../codecs/test_decode_example.json | 0 .../codecs/test_encode_example.json | 0 vendors/{example-template => example-vendor}/devices/example.toml | 0 .../profiles/example-EU868.toml | 0 vendors/{example-template => example-vendor}/vendor.toml | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename vendors/{example-template => example-vendor}/codecs/example.js (100%) rename vendors/{example-template => example-vendor}/codecs/test_decode_example.json (100%) rename vendors/{example-template => example-vendor}/codecs/test_encode_example.json (100%) rename vendors/{example-template => example-vendor}/devices/example.toml (100%) rename vendors/{example-template => example-vendor}/profiles/example-EU868.toml (100%) rename vendors/{example-template => example-vendor}/vendor.toml (100%) diff --git a/vendors/example-template/codecs/example.js b/vendors/example-vendor/codecs/example.js similarity index 100% rename from vendors/example-template/codecs/example.js rename to vendors/example-vendor/codecs/example.js diff --git a/vendors/example-template/codecs/test_decode_example.json b/vendors/example-vendor/codecs/test_decode_example.json similarity index 100% rename from vendors/example-template/codecs/test_decode_example.json rename to vendors/example-vendor/codecs/test_decode_example.json diff --git a/vendors/example-template/codecs/test_encode_example.json b/vendors/example-vendor/codecs/test_encode_example.json similarity index 100% rename from vendors/example-template/codecs/test_encode_example.json rename to vendors/example-vendor/codecs/test_encode_example.json diff --git a/vendors/example-template/devices/example.toml b/vendors/example-vendor/devices/example.toml similarity index 100% rename from vendors/example-template/devices/example.toml rename to vendors/example-vendor/devices/example.toml diff --git a/vendors/example-template/profiles/example-EU868.toml b/vendors/example-vendor/profiles/example-EU868.toml similarity index 100% rename from vendors/example-template/profiles/example-EU868.toml rename to vendors/example-vendor/profiles/example-EU868.toml diff --git a/vendors/example-template/vendor.toml b/vendors/example-vendor/vendor.toml similarity index 100% rename from vendors/example-template/vendor.toml rename to vendors/example-vendor/vendor.toml From 788ad7cff993d4f02dff08db5120b9c06ae0eb7c Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 30 Aug 2024 11:06:19 +0100 Subject: [PATCH 3/7] Document examples. Add validators. Add CI workflow. --- .github/workflows/main.yml | 38 +++++++++++++ test-runner/src/structs.rs | 18 ++++-- vendors/example-vendor/codecs/example.js | 3 + vendors/example-vendor/devices/example.toml | 18 ++++++ .../profiles/example-EU868.toml | 56 +++++++++++++++++++ vendors/example-vendor/vendor.toml | 14 +++++ 6 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..426cfd6 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,38 @@ +name: CI +on: + push: + branches: + - '*' + tags: + - 'v*' + pull_request: + +env: + CARGO_INCREMENTAL: 0 + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-24.05 + - + name: Cargo cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + test-runner/target/ + key: ${{ runner.os }}-cargo-test-${{ matrix.database }}-${{ hashFiles('**/Cargo.lock') }} + - + name: Run tests + run: nix-shell --command "make test" diff --git a/test-runner/src/structs.rs b/test-runner/src/structs.rs index 34e6c7d..cdc680e 100644 --- a/test-runner/src/structs.rs +++ b/test-runner/src/structs.rs @@ -42,34 +42,42 @@ pub struct ProfileConfiguration { #[derive(Default, Deserialize, Validate)] #[serde(default)] pub struct Profile { - #[validate(enumerate = ["EU868"])] + #[validate(enumerate = ["EU868", "US915", "CN779", "EU433", "AU915", "CN470", "AS923", "AS923-2", "AS923-3", "AS923-4", "KR920", "IN865", "RU864"])] pub region: String, - #[validate(enumerate = ["1.0.4"])] + #[validate(enumerate = ["1.0.0", "1.0.1", "1.0.2", "1.0.3","1.0.4", "1.1.0"])] pub mac_version: String, - #[validate(enumerate = ["RP002-1.0.3"])] + #[validate(enumerate = ["A", "B", "RP002-1.0.0", "RP002-1.0.1", "RP002-1.0.2", "RP002-1.0.3", "RP002-1.0.4"])] pub reg_params_revision: String, pub supports_otaa: bool, pub supports_class_b: bool, pub supports_class_c: bool, pub max_eirp: usize, + #[validate] pub abp: ProfileAbp, + #[validate] pub class_b: ProfileClassB, pub class_c: ProfileClassC, } -#[derive(Default, Deserialize)] +#[derive(Default, Deserialize, Validate)] pub struct ProfileAbp { + #[validate(maximum = 15)] pub rx1_delay: usize, + #[validate(maximum = 7)] pub rx1_dr_offset: usize, + #[validate(maximum = 15)] pub rx2_dr: usize, pub rx2_freq: usize, } -#[derive(Default, Deserialize)] +#[derive(Default, Deserialize, Validate)] pub struct ProfileClassB { pub timeout_secs: usize, + + #[validate(maximum = 7)] pub ping_slot_nb_k: usize, + #[validate(maximum = 15)] pub ping_slot_dr: usize, pub ping_slot_freq: usize, } diff --git a/vendors/example-vendor/codecs/example.js b/vendors/example-vendor/codecs/example.js index 9e7822c..a7cedbd 100644 --- a/vendors/example-vendor/codecs/example.js +++ b/vendors/example-vendor/codecs/example.js @@ -1,3 +1,6 @@ +// For more information, please refer to: +// https://resources.lora-alliance.org/technical-specifications/ts013-1-0-0-payload-codec-api + function decodeUplink(input) { return { data: { diff --git a/vendors/example-vendor/devices/example.toml b/vendors/example-vendor/devices/example.toml index 8bf2698..46d5b45 100644 --- a/vendors/example-vendor/devices/example.toml +++ b/vendors/example-vendor/devices/example.toml @@ -1,8 +1,26 @@ [device] + # Device name. name = "Example device" + + # Device description. description = "This is an example device" + # Device firmware version. + # + # This section can be repeated in case multiple firmware versions exist. + # As a new firmware version can change the supported profiles / regions and + # payload format, each firmware version has its own profiles and codec + # configuration. [[device.firmware]] + # Firmware version. version = "1.0.0" + + # List of supported profiles. + # + # This list refers to one or multiple profiles in the profiles/ directory. profiles = ["example-EU868.toml"] + + # Payload codec. + # + # In case no codec is available, you can remove this option. codec = "example.js" diff --git a/vendors/example-vendor/profiles/example-EU868.toml b/vendors/example-vendor/profiles/example-EU868.toml index d7527d6..3a0618a 100644 --- a/vendors/example-vendor/profiles/example-EU868.toml +++ b/vendors/example-vendor/profiles/example-EU868.toml @@ -1,23 +1,79 @@ [profile] + # Region Common-name. + # + # Please refer to the LoRaWAN Regional Parameters specification for the available + # common-names. region = "EU868" + + # LoRaWAN mac-version (1.x.y). mac_version = "1.0.4" + + # LoRaWAN Regional Parameters revision. + # + # Examples: A, B, RP002-1.x.y. reg_params_revision = "RP002-1.0.3" + + # Device supports OTAA. supports_otaa = true + + # Device supports Class-B. + # + # If set to true, do not forget to configure class_b section below. supports_class_b = false + + # Device supports Class-C. + # + # If set to true, do not forget to configure class_c section below. supports_class_c = false + + # Max EIRP supported by device. max_eirp = 16 + # ABP settings. + # + # This section must be configured in case supports_otaa is set to false. [profile.abp] + # RX1 Delay. rx1_delay = 1 + + # RX1 DR offset. rx1_dr_offset = 0 + + # RX2 DR. rx2_dr = 0 + + # RX2 frequency (Hz). rx2_freq = 869525000 + + # Class-B settings. + # + # This section must be configured in case supports_class_b is set to true. [profile.class_b] + # Timeout in seconds. + # + # In case of an confirmed downlink, the device is expected to respond with + # an ack within the given amount of time. timeout_secs = 120 + + # Ping-slot numbers (k). + # + # The actual amount is 2^k. Valid options are: 0 - 7. ping_slot_nb_k = 0 + + # Ping-slot DR. ping_slot_dr = 0 + + # Ping-slot frequency (Hz). ping_slot_freq = 869525000 + + # Class-C settings. + # + # This section must be configured in case supports_class_c is set to true. [profile.class_c] + # Timeout in seconds. + # + # In case of an confirmed downlink, the device is expected to respond with + # an ack within the given amount of time. timeout_secs = 120 diff --git a/vendors/example-vendor/vendor.toml b/vendors/example-vendor/vendor.toml index a783e2a..4cd119b 100644 --- a/vendors/example-vendor/vendor.toml +++ b/vendors/example-vendor/vendor.toml @@ -1,5 +1,19 @@ [vendor] + # Vendor name. name = "Example vendor" + + # LoRa Alliance assigned Vendor ID. + # + # See also: + # ttps://resources.lora-alliance.org/document/lora-alliance-vendor-id-20230915 id = 0 + + # OUIs owned by vendor (optional). + # + # Example: ouis = ["010203", "40506"] ouis = [] + + # List of devices. + # + # This list refers to one or multiple devices in the devices/ directory. devices = ["example.toml"] From 1a4cb70701790931130f87974af51dc8fe418ea4 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 30 Aug 2024 11:32:28 +0100 Subject: [PATCH 4/7] Update README. --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0b2f57..dc740e3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,80 @@ -# LoRaWAN(R) device-profiles repository +# LoRaWAN(R) device-profiles +This repository contains device-profiles for LoRaWAN devices grouped by +vendor. A device-profile contains important information about the capabilities +of the device. For example which LoRaWAN mac-version has been implemented, +which regions are supported, if the device supports Class-B or Class-C, etc... +The aim is to build a complete list of LoRaWAN device-profiles that then can +be imported by ChirpStack or potentially any other LNS. + +## Why not use the TTN lorawan-devices repository? + +Unfortuantely the [https://github.com/thethingsnetwork/lorawan-devices](https://github.com/thethingsnetwork/lorawan-devices) +repository can no longer be used as a whole. The open-source license has been +removed and users are not allowed to _extract and/or reuse the Device Repositoryas +as a whole or a substantial part of its content_. +This prevents ChirpStack users (and other LNS providers) from importing this +repository into their database. + +The goal of this repository is to provide an open-source database of LoRaWAN +device-profiles that can be freely imported. + +## Structure + +Example structure for an `example-vendor` with an `example` device: + +``` +vendors/ +└── example-vendor + ├── codecs + │   ├── example.js + │   ├── test_decode_example.json + │   └── test_encode_example.json + ├── devices + │   └── example.toml + ├── profiles + │   └── example-EU868.toml + └── vendor.toml +``` + +Please take a look at the `vendors/example-vendor` example documented +configuration files. + +### `vendors/example-vendor` + +This is the root of the example vendor. It must contain a `vendor.toml` +file. This `vendor.toml`. + +### `vendors/example-vendor/codecs` + +This directory contains the payload codecs. Codecs can be used by one or +multiple devices. E.g. some vendors have a generic payload codec. + +Each codec is expected to have tests for encoding and decoding. If the +codec filename is `example.js`, then you should create two test-files +called `test_decode_example.json` (thus + `test_decode_` prefix and `.json` +extension) and `test_encode_example.json`. + +### `vendors/example-vendor/devices` + +This directory contains the devices. Each device will have its own `.toml` +configuration. + +### `vendors/example-vendor/profiles` + +This directory contains the profiles. These profiles can be used by one +or multiple devices. The profile also defines the region. + +## Running the test + +To run the test-runner, execute: + +``` +make test +``` + +This will run all tests within a Docker Compose environment. + +## License + +This repository is distributed under the MIT license. See also `LICENSE`. From 4d2342f4ee189fd40f0e9dc6f1444548604d8fb1 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 30 Aug 2024 11:36:21 +0100 Subject: [PATCH 5/7] Update workflow. --- .github/workflows/main.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 426cfd6..91a9355 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,11 +17,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v27 - with: - nix_path: nixpkgs=channel:nixos-24.05 - name: Cargo cache uses: actions/cache@v4 @@ -32,7 +27,7 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ test-runner/target/ - key: ${{ runner.os }}-cargo-test-${{ matrix.database }}-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} - name: Run tests run: nix-shell --command "make test" From 0aca855a4c8ea31733a09310b84432dc3ff7b65b Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 30 Aug 2024 11:38:38 +0100 Subject: [PATCH 6/7] Update workflow. --- .github/workflows/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 91a9355..3fa436e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - + name: Install Nix + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-24.05 - name: Cargo cache uses: actions/cache@v4 @@ -30,4 +35,4 @@ jobs: key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} - name: Run tests - run: nix-shell --command "make test" + run: nix-shell --command "cd test-runner && cargo run" From cebd7823af63f49cb3fe3b1db39f6300e2b7f521 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Fri, 30 Aug 2024 11:54:26 +0100 Subject: [PATCH 7/7] Add vendor + device metadata config. --- test-runner/src/main.rs | 1 + test-runner/src/structs.rs | 20 ++++++++++++++++++-- vendors/example-vendor/devices/example.toml | 9 +++++++++ vendors/example-vendor/vendor.toml | 5 +++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/test-runner/src/main.rs b/test-runner/src/main.rs index 106b20e..6d95797 100644 --- a/test-runner/src/main.rs +++ b/test-runner/src/main.rs @@ -4,6 +4,7 @@ use std::path::Path; use serde_valid::Validate; mod js; +#[allow(dead_code)] mod structs; fn main() { diff --git a/test-runner/src/structs.rs b/test-runner/src/structs.rs index cdc680e..c47d702 100644 --- a/test-runner/src/structs.rs +++ b/test-runner/src/structs.rs @@ -6,12 +6,20 @@ pub struct VendorConfiguration { pub vendor: Vendor, } -#[derive(Deserialize)] +#[derive(Default, Deserialize)] +#[serde(default)] pub struct Vendor { pub name: String, pub id: usize, pub ouis: Vec, pub devices: Vec, + pub metadata: VendorMetadata, +} + +#[derive(Default, Deserialize)] +#[serde(default)] +pub struct VendorMetadata { + pub homepage: Option, } #[derive(Deserialize)] @@ -19,11 +27,19 @@ pub struct DeviceConfiguration { pub device: Device, } -#[derive(Deserialize)] +#[derive(Default, Deserialize)] +#[serde(default)] pub struct Device { pub name: String, pub description: String, pub firmware: Vec, + pub metadata: DeviceMetadata, +} + +#[derive(Default, Deserialize)] +pub struct DeviceMetadata { + pub product_url: Option, + pub documentation_url: Option, } #[derive(Deserialize)] diff --git a/vendors/example-vendor/devices/example.toml b/vendors/example-vendor/devices/example.toml index 46d5b45..3ddefe3 100644 --- a/vendors/example-vendor/devices/example.toml +++ b/vendors/example-vendor/devices/example.toml @@ -5,6 +5,15 @@ # Device description. description = "This is an example device" + # Device metadata (optional). + [device.metadata] + # Product URL. + product_url = "https://www.example.com/devices/example-device" + + # Documentation URL. + documentation_url = "https://docs.example.com/devices/example-device" + + # Device firmware version. # # This section can be repeated in case multiple firmware versions exist. diff --git a/vendors/example-vendor/vendor.toml b/vendors/example-vendor/vendor.toml index 4cd119b..de12d8d 100644 --- a/vendors/example-vendor/vendor.toml +++ b/vendors/example-vendor/vendor.toml @@ -17,3 +17,8 @@ # # This list refers to one or multiple devices in the devices/ directory. devices = ["example.toml"] + + # Vendor metadata (optional). + [vendor.metadata] + # Vendor homepage. + homepage = "https://www.example.com"