diff --git a/ruby_tracklib/.gitignore b/ruby_tracklib/.gitignore index d14bd38..5dd8129 100644 --- a/ruby_tracklib/.gitignore +++ b/ruby_tracklib/.gitignore @@ -1,7 +1,11 @@ -mkmf.log -tracklib*.gem -target/ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ .nix-gems/ -pkg/ -lib/tracklib.so -Gemfile.lock +target/ +*.gem diff --git a/ruby_tracklib/Cargo.lock b/ruby_tracklib/Cargo.lock index d9df8e3..17f2065 100644 --- a/ruby_tracklib/Cargo.lock +++ b/ruby_tracklib/Cargo.lock @@ -1,254 +1,309 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] -name = "backtrace" -version = "0.3.33" +name = "Inflector" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] -name = "backtrace-sys" -version = "0.1.31" +name = "aliasable" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] -name = "base64" -version = "0.10.1" +name = "arrayvec" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] -name = "build_const" -version = "0.2.1" +name = "autocfg" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "byteorder" -version = "1.3.2" +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "cc" -version = "1.0.38" +name = "crc" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" +dependencies = [ + "crc-catalog", +] [[package]] -name = "cfg-if" -version = "0.1.9" +name = "crc-catalog" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" [[package]] -name = "crc" -version = "1.8.1" +name = "ct-codecs" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" [[package]] -name = "itoa" -version = "0.4.4" +name = "fiat-crypto" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35354cf6bf9d259374646f419a25c7dd0bb208d291e44dc73db557542fe017fc" + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] [[package]] name = "lazy_static" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "leb128" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.60" +version = "0.2.124" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" [[package]] -name = "log" -version = "0.4.8" +name = "memchr" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] -name = "memchr" -version = "2.2.1" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nom" -version = "4.2.3" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ - "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", + "minimal-lexical", ] [[package]] -name = "proc-macro2" -version = "0.4.30" +name = "nom-leb128" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a73b6c3a9ecfff12ad50dedba051ef838d2f478d938bb3e6b3842431028e62" dependencies = [ - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "arrayvec", + "nom", + "num-traits", ] [[package]] -name = "quote" -version = "0.6.13" +name = "num-traits" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", ] [[package]] -name = "ruby_tracklib" -version = "0.1.0" +name = "orion" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efc4e7f3220d326a2d87d507510b20b0ecd5469f80f6e985dd6fcefa2de9af1" dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rutie 0.7.0 (git+https://github.com/danlarkin/rutie)", - "rutie-serde 0.1.1 (git+https://github.com/danlarkin/rutie-serde)", - "tracklib 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ct-codecs", + "fiat-crypto", + "getrandom", + "subtle", + "zeroize", ] [[package]] -name = "rustc-demangle" -version = "0.1.15" +name = "ouroboros" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f31a3b678685b150cba82b702dcdc5e155893f63610cf388d30cd988d4ca2bf" +dependencies = [ + "aliasable", + "ouroboros_macro", + "stable_deref_trait", +] [[package]] -name = "rutie" -version = "0.7.0" -source = "git+https://github.com/danlarkin/rutie#6db74585735ca82894d6163357ee4defdc2cdc61" +name = "ouroboros_macro" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084fd65d5dd8b3772edccb5ffd1e4b7eba43897ecd0f9401e330e8c542959408" dependencies = [ - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rutie-serde" -version = "0.1.1" -source = "git+https://github.com/danlarkin/rutie-serde#de14caea6c73cc479438c0c2b27b528d498e77fc" +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rutie 0.7.0 (git+https://github.com/danlarkin/rutie)", - "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", ] [[package]] -name = "ryu" -version = "1.0.0" +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 = "serde" -version = "1.0.98" +name = "proc-macro2" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] [[package]] -name = "serde_json" -version = "1.0.40" +name = "quote" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ - "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] -name = "snafu" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "ruby_tracklib" +version = "2.0.0" dependencies = [ - "backtrace 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)", - "snafu-derive 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "ouroboros", + "rutie", + "tracklib", ] [[package]] -name = "snafu-derive" -version = "0.2.3" +name = "rutie" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d97db4cbb9739b48364c38cc9a6ebabdc07b42bd87b60ab448e1f29eaebb2ac" dependencies = [ - "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "libc", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" -version = "0.15.42" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ - "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] -name = "tracklib" -version = "0.1.0" +name = "thiserror" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "leb128 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", - "snafu 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracklib" +version = "2.0.0" +dependencies = [ + "crc", + "leb128", + "nom", + "nom-leb128", + "orion", + "thiserror", ] [[package]] name = "unicode-xid" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum backtrace 0.3.33 (registry+https://github.com/rust-lang/crates.io-index)" = "88fb679bc9af8fa639198790a77f52d345fe13656c08b43afa9424c206b731c6" -"checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" -"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" -"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" -"checksum cc 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "ce400c638d48ee0e9ab75aef7997609ec57367ccfe1463f21bf53c3eca67bf46" -"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" -"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" -"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" -"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" -"checksum leb128 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3576a87f2ba00f6f106fdfcd16db1d698d648a26ad8e0573cad8537c3c362d2a" -"checksum libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d44e80633f007889c7eff624b709ab43c92d708caad982295768a7b13ca3b5eb" -"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" -"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" -"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" -"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -"checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" -"checksum rutie 0.7.0 (git+https://github.com/danlarkin/rutie)" = "" -"checksum rutie-serde 0.1.1 (git+https://github.com/danlarkin/rutie-serde)" = "" -"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" -"checksum serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5626ac617da2f2d9c48af5515a21d5a480dbd151e01bb1c355e26a3e68113" -"checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704" -"checksum snafu 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67a2e16c9b74a09d4bc84ea6e5eb68fef70b4cb14e8ccd26f2de76af2e9c2e8a" -"checksum snafu-derive 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bf96650c2b31fa949780f792025f4ca79fbe87bd723a8b3573dce37be7193b08" -"checksum syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)" = "eadc09306ca51a40555dd6fc2b415538e9e18bc9f870e47b1a524a79fe2dcf5e" -"checksum tracklib 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97f6f1e9e410c9b8d6b96f1eab38296f3c6aa8cd67d71a5163db6ba2bef9b6e5" -"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" -"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "zeroize" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb5728b8afd3f280a869ce1d4c554ffaed35f45c231fc41bfbd0381bef50317" diff --git a/ruby_tracklib/Cargo.toml b/ruby_tracklib/Cargo.toml index 928d9a4..e0391c3 100644 --- a/ruby_tracklib/Cargo.toml +++ b/ruby_tracklib/Cargo.toml @@ -1,17 +1,14 @@ [package] name = "ruby_tracklib" -version = "0.1.0" -authors = ["Dan Larkin "] -license = "Apache-2.0 OR MIT" -edition = "2018" +version = "2.0.0" +edition = "2021" [lib] -name = "tracklib" -crate-type = ["cdylib"] +name = "ruby_tracklib" +crate-type = ["cdylib", "lib"] [dependencies] -tracklib = "*" -rutie = {git="https://github.com/danlarkin/rutie", features=["no-link"]} -rutie-serde = {git="https://github.com/danlarkin/rutie-serde"} -lazy_static = "1.3" -base64 = "0.10" +lazy_static = "1.4" +ouroboros = "0.15" +rutie = "0.8" +tracklib = {path="../tracklib"} diff --git a/ruby_tracklib/Gemfile b/ruby_tracklib/Gemfile index 9921f0a..3be9c3c 100644 --- a/ruby_tracklib/Gemfile +++ b/ruby_tracklib/Gemfile @@ -1,4 +1,2 @@ source "https://rubygems.org" - -# Specify your gem's dependencies in tracklib.gemspec gemspec diff --git a/ruby_tracklib/Gemfile.lock b/ruby_tracklib/Gemfile.lock new file mode 100644 index 0000000..0171f2a --- /dev/null +++ b/ruby_tracklib/Gemfile.lock @@ -0,0 +1,36 @@ +PATH + remote: . + specs: + tracklib (2.0.0) + rutie (~> 0.0.4) + +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.4.4) + rake (13.0.6) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.3) + rutie (0.0.4) + +PLATFORMS + ruby + +DEPENDENCIES + rake + rspec + tracklib! + +BUNDLED WITH + 2.4.15 diff --git a/ruby_tracklib/Rakefile b/ruby_tracklib/Rakefile index 8663948..2ecd382 100644 --- a/ruby_tracklib/Rakefile +++ b/ruby_tracklib/Rakefile @@ -1,16 +1,18 @@ -# coding: utf-8 -require "rbconfig" -require "bundler/gem_tasks" -require "rake/testtask" -require "thermite/tasks" require 'rspec/core/rake_task' -thermite = Thermite::Tasks.new -RSpec::Core::RakeTask.new(:spec) +desc 'Build Rust extension' +task :build_lib do + sh 'cargo build --release' +end -desc 'Run Rust & Ruby testsuites' -task test: ['thermite:build', 'thermite:test', 'spec'] do +desc 'bundle install' +task :bundle_install do + sh 'bundle install' +end +RSpec::Core::RakeTask.new(spec: [:bundle_install, :build_lib]) do |t| + t.pattern = "spec/**/*_spec.rb" end -task :default => :test +task :default => :build_lib +task :test => :spec diff --git a/ruby_tracklib/bin/console b/ruby_tracklib/bin/console deleted file mode 100755 index 70bf21a..0000000 --- a/ruby_tracklib/bin/console +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env ruby - -require "bundler/setup" -require "tracklib" - -# You can add fixtures and/or initialization code here to make experimenting -# with your gem easier. You can also use a different console, if you like. - -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - -require "irb" -IRB.start(__FILE__) diff --git a/ruby_tracklib/bin/setup b/ruby_tracklib/bin/setup deleted file mode 100755 index dce67d8..0000000 --- a/ruby_tracklib/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here diff --git a/ruby_tracklib/default.nix b/ruby_tracklib/default.nix index 73d0b98..d1c7c22 100644 --- a/ruby_tracklib/default.nix +++ b/ruby_tracklib/default.nix @@ -3,10 +3,11 @@ stdenv.mkDerivation rec { name = "tracklib"; env = buildEnv { name = name; paths = buildInputs; }; buildInputs = [ - ruby_2_4 + ruby_3_1 ]; shellHook = '' mkdir -p .nix-gems + export IRBRC=irbrc.rb export GEM_HOME=$PWD/.nix-gems export GEM_PATH=$GEM_HOME export PATH=$GEM_HOME/bin:$PATH diff --git a/ruby_tracklib/ext/Rakefile b/ruby_tracklib/ext/Rakefile deleted file mode 100644 index 0252dbf..0000000 --- a/ruby_tracklib/ext/Rakefile +++ /dev/null @@ -1,7 +0,0 @@ -require 'thermite/tasks' - -project_toplevel_dir = File.dirname(__dir__) -Thermite::Tasks.new(cargo_project_path: project_toplevel_dir, - ruby_project_path: project_toplevel_dir) - -task default: 'thermite:build' diff --git a/ruby_tracklib/irbrc.rb b/ruby_tracklib/irbrc.rb new file mode 100644 index 0000000..4776dfc --- /dev/null +++ b/ruby_tracklib/irbrc.rb @@ -0,0 +1,8 @@ +if ENV["INSIDE_EMACS"] then + IRB.conf[:USE_MULTILINE] = nil + IRB.conf[:USE_SINGLELINE] = false + IRB.conf[:PROMPT_MODE] = :INF_RUBY + + IRB.conf[:USE_READLINE] = false + IRB.conf[:USE_COLORIZE] = true +end diff --git a/ruby_tracklib/lib/tracklib.rb b/ruby_tracklib/lib/tracklib.rb index 5b8ed5f..7c86011 100644 --- a/ruby_tracklib/lib/tracklib.rb +++ b/ruby_tracklib/lib/tracklib.rb @@ -2,13 +2,7 @@ require "rutie" module Tracklib - class UnknownFieldError < StandardError; end - Rutie.new(:tracklib, {lib_path: "../lib", lib_prefix: ""}).init 'Init_Tracklib', __dir__ -end - -class RWTFile - def hello_from_ruby() - puts "I am a ruby method" - return 5 + unless defined?(TrackReader) + Rutie.new(:ruby_tracklib).init 'Init_Tracklib', __dir__ end end diff --git a/ruby_tracklib/lib/tracklib/version.rb b/ruby_tracklib/lib/tracklib/version.rb index acf35a3..c604b73 100644 --- a/ruby_tracklib/lib/tracklib/version.rb +++ b/ruby_tracklib/lib/tracklib/version.rb @@ -1,3 +1,3 @@ module Tracklib - VERSION = "0.1.7" + VERSION = "2.0.0".freeze end diff --git a/ruby_tracklib/rustfmt.toml b/ruby_tracklib/rustfmt.toml new file mode 100644 index 0000000..965afc8 --- /dev/null +++ b/ruby_tracklib/rustfmt.toml @@ -0,0 +1,3 @@ +max_width = 120 +use_field_init_shorthand = true +use_try_shorthand = true diff --git a/ruby_tracklib/spec/basic_spec.rb b/ruby_tracklib/spec/basic_spec.rb deleted file mode 100644 index 5d1ebc9..0000000 --- a/ruby_tracklib/spec/basic_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -require "spec_helper" - -CONFIG = {"track_points"=>{"LongFloat"=>["x", "y", "e"], - "ShortFloat"=>["s", "d"], - "Number"=>["t", "c", "h"], - "Base64"=>["ep"]}, - "course_points"=>{}} - -def roundtrip(data, field_config) - rwtf = RWTFile::from_h(data, field_config) - bytes = rwtf.to_bytes - new_rwtf = RWTFile::from_bytes(bytes) - return new_rwtf.to_h -end - -describe RWTFile do - context "round trips and" do - it "drops empty keys" do - orig_data = {"track_points"=>[{""=>3, - "t"=>4, - "x"=>4.4, - "y"=>52.1, - "e"=>-22.7}]} - expect(roundtrip(orig_data, CONFIG)).to eq({"track_points"=>[{"t"=>4, - "x"=>4.4, - "y"=>52.1, - "e"=>-22.7}]}) - end - it "doesn't drop a point" do - orig_data = {"track_points"=>[{"t"=>1, - "e"=>40}, - {}, - {"t"=>nil}, - {"t"=>4}]} - expect(roundtrip(orig_data, CONFIG)).to eq({"track_points"=>[{"t"=>1, - "e"=>40}, - {}, - {}, - {"t"=>4}]}) - end - it "drops numbers that are too large to turn into primitives" do - # integer - orig_data = {"track_points"=>[{"x"=>10000000000000000000000000000000000, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq({"track_points"=>[{"y"=>7.2}]}) - - # float - orig_data = {"track_points"=>[{"x"=>10000000000000000000000000000000000.3, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq({"track_points"=>[{"y"=>7.2}]}) - end - it "drops numbers that are valid primitives but still too large for tracklib" do - ## Numbers - # drops 2**60 - orig_data = {"track_points"=>[{"t"=>2**60, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq({"track_points"=>[{"y"=>7.2}]}) - # allows 2**48-1 - orig_data = {"track_points"=>[{"t"=>2**48-1, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq(orig_data) - # allows floats even though they should be ints - orig_data = {"track_points"=>[{"t"=>2.0**48-1.0, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq(orig_data) - - ## LongFloat - # drops 2.0**48-1 - orig_data = {"track_points"=>[{"x"=>2.0**48-1.0, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq({"track_points"=>[{"y"=>7.2}]}) - # allows 2.0**24-1 - orig_data = {"track_points"=>[{"x"=>2.0**24-1.0, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq(orig_data) - # allows ints even though they should be floats - orig_data = {"track_points"=>[{"x"=>2**24-1, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq(orig_data) - - ## ShortFloat - # drops 2.0**48-1 - orig_data = {"track_points"=>[{"s"=>2.0**48-1.0, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq({"track_points"=>[{"y"=>7.2}]}) - # allows 2.0**38-1 - orig_data = {"track_points"=>[{"s"=>2.0**38-1.0, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq(orig_data) - # allows ints even though they should be floats - orig_data = {"track_points"=>[{"s"=>2**38-1, "y"=>7.2}]} - expect(roundtrip(orig_data, CONFIG)).to eq(orig_data) - end - end -end diff --git a/ruby_tracklib/spec/reader_spec.rb b/ruby_tracklib/spec/reader_spec.rb new file mode 100644 index 0000000..7dd3d35 --- /dev/null +++ b/ruby_tracklib/spec/reader_spec.rb @@ -0,0 +1,455 @@ +require "spec_helper" + +data = [ + # Header + 0x89, # rwtfmagic + 0x52, + 0x57, + 0x54, + 0x46, + 0x0A, + 0x1A, + 0x0A, + 0x01, # file version + 0x00, # fv reserve + 0x00, + 0x00, + 0x00, # creator version + 0x00, # cv reserve + 0x00, + 0x00, + 0x18, # metadata table offset + 0x00, + 0x22, # data offset + 0x00, + 0x00, # e reserve + 0x00, + 0x88, # header crc + 0x64, + + # Metadata Table + 0x02, # one entry + 0x00, # entry type: track_type + 0x02, # entry size + 0x02, # track type: segment + 0x05, # segment id + 0x01, # entry type: created_at + 0x01, # entry size + 0x19, # timestamp + 0xD7, # crc + 0x59, + + # Data Table + 0x02, # two sections + + # Data Table Section 1 + 0x00, # section encoding = standard + 0x05, # leb128 point count + 0x84, # leb128 data size + 0x01, + + # Schema for Section 1 + 0x00, # schema version + 0x08, # field count + 0x00, # first field type = I64 + 0x03, # name len + 'i'.ord, # name + '6'.ord, + '4'.ord, + 0x09, # data size + 0x01, # second field type = F64 + 0x02, # scale + 0x05, # name len + 'f'.ord, # name + '6'.ord, + '4'.ord, + ':'.ord, + '2'.ord, + 0x0A, # data len + 0x02, # third field type = U64 + 0x03, # name len + 'u'.ord, # name + '6'.ord, + '4'.ord, + 0x09, # data len + 0x10, # fourth field type = Bool + 0x04, # name len + 'b'.ord, # name + 'o'.ord, + 'o'.ord, + 'l'.ord, + 0x09, # data len + 0x20, # fifth field type = String + 0x06, # name len + 's'.ord, # name + 't'.ord, + 'r'.ord, + 'i'.ord, + 'n'.ord, + 'g'.ord, + 0x18, # data len + 0x21, # sixth field type = Bool Array + 0x0A, # name len + 'b'.ord, # name + 'o'.ord, + 'o'.ord, + 'l'.ord, + ' '.ord, + 'a'.ord, + 'r'.ord, + 'r'.ord, + 'a'.ord, + 'y'.ord, + 0x0E, # data len + 0x22, # seventh field type = U64 Array + 0x09, # name len + 'u'.ord, # name + '6'.ord, + '4'.ord, + ' '.ord, + 'a'.ord, + 'r'.ord, + 'r'.ord, + 'a'.ord, + 'y'.ord, + 0x18, # data len + 0x23, # eigth field type = Byte Array + 0x0A, # name len + 'b'.ord, # name + 'y'.ord, + 't'.ord, + 'e'.ord, + ' '.ord, + 'a'.ord, + 'r'.ord, + 'r'.ord, + 'a'.ord, + 'y'.ord, + 0x18, # data len + + # Data Table Section 2 + 0x00, # section encoding = standard + 0x03, # leb128 point count + 0x26, # leb128 data size + + # Schema for Section 2 + 0x00, # schema version + 0x03, # field count + 0x00, # first field type = I64 + 0x01, # name length + 'a'.ord, # name + 0x07, # leb128 data size + 0x10, # second field type = Bool + 0x01, # name length + 'b'.ord, # name + 0x06, # leb128 data size + 0x20, # third field type = String + 0x01, # name length + 'c'.ord, # name + 0x12, # leb128 data size + + # Data Table CRC + 0xCD, + 0xB9, + + # Data Section 1 + + # Presence Column + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111, + 0x4B, # crc + 0xBF, + 0x08, + 0x4E, + + # Data Column 1 = I64 + 0x2A, # 42 + 0x00, # no change + 0x00, # no change + 0x00, # no change + 0x00, # no change + 0xD0, # crc + 0x8D, + 0x79, + 0x68, + + # Data Column 2 = F64 + 0xCE, # 0.78 + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3C, # crc + 0x2E, + 0x7B, + 0x33, + + # Data Column 3 = U64 + 0x19, # 25 + 0x00, + 0x00, + 0x00, + 0x00, + 0xE4, # crc + 0x2A, + 0xD9, + 0x33, + + # Data Column 4 = Bool + 0x01, # true + 0x01, # true + 0x01, # true + 0x01, # true + 0x01, # true + 0xB5, # crc + 0xC9, + 0x8F, + 0xFA, + + # Data Column 5 = String + 0x03, # length 3 + 'h'.ord, + 'e'.ord, + 'y'.ord, + 0x03, # length 3 + 'h'.ord, + 'e'.ord, + 'y'.ord, + 0x03, # length 3 + 'h'.ord, + 'e'.ord, + 'y'.ord, + 0x03, # length 3 + 'h'.ord, + 'e'.ord, + 'y'.ord, + 0x03, # length 3 + 'h'.ord, + 'e'.ord, + 'y'.ord, + 0x36, # crc + 0x71, + 0x24, + 0x0B, + + # Data Column 6 = Bool Array + 0x01, # array len + 0x01, # true + 0x01, # array len + 0x01, # true + 0x01, # array len + 0x01, # true + 0x01, # array len + 0x01, # true + 0x01, # array len + 0x01, # true + 0xB3, # crc + 0x6F, + 0x38, + 0x51, + + # Data Column 7 = U64 Array + 0x03, # array len + 0x0C, # 12 + 0x7E, # -2 + 0x03, # +3 + 0x03, # array len + 0x0C, # 12 + 0x7E, # -2 + 0x03, # +3 + 0x03, # array len + 0x0C, # 12 + 0x7E, # -2 + 0x03, # +3 + 0x03, # array len + 0x0C, # 12 + 0x7E, # -2 + 0x03, # +3 + 0x03, # array len + 0x0C, # 12 + 0x7E, # -2 + 0x03, # +3 + 0xD1, # crc + 0xB4, + 0x14, + 0x37, + + # Data Column 8 = Byte Array + 0x03, # array len + 0x0C, # 12 + 0x0A, # 10 + 0x0D, # 13 + 0x03, # array len + 0x0C, # 12 + 0x0A, # 10 + 0x0D, # 13 + 0x03, # array len + 0x0C, # 12 + 0x0A, # 10 + 0x0D, # 13 + 0x03, # array len + 0x0C, # 12 + 0x0A, # 10 + 0x0D, # 13 + 0x03, # array len + 0x0C, # 12 + 0x0A, # 10 + 0x0D, # 13 + 0x94, # crc + 0x1D, + 0x88, + 0xAB, + + # Data Section 2 + + # Presence Column + 0b00000111, + 0b00000101, + 0b00000111, + 0x1A, # crc + 0x75, + 0xEA, + 0xC4, + + # Data Column 1 = I64 + 0x01, # 1 + 0x01, # 2 + 0x02, # 4 + 0xCA, # crc + 0xD4, + 0xD8, + 0x92, + + # Data Column 2 = Bool + 0x00, # false + # None + 0x01, # true + 0x35, # crc + 0x86, + 0x89, + 0xFB, + + # Data Column 3 = String + 0x04, # length 4 + 'R'.ord, + 'i'.ord, + 'd'.ord, + 'e'.ord, + 0x04, # length 4 + 'w'.ord, + 'i'.ord, + 't'.ord, + 'h'.ord, + 0x03, # length 3 + 'G'.ord, + 'P'.ord, + 'S'.ord, + 0xA3, # crc + 0x02, + 0xEC, + 0x48 +].pack("c*") + +describe Tracklib do + it "can read metadata" do + track_reader = Tracklib::TrackReader.new(data) + expect(track_reader.metadata()).to eq([[:track_type, :segment, 5], + [:created_at, Time.new(1970, 1, 1, 0, 0, 25, "UTC")]]) + end + + it "can read versions" do + track_reader = Tracklib::TrackReader.new(data) + expect(track_reader.file_version()).to eq(1) + expect(track_reader.creator_version()).to eq(0) + end + + it "can iterate through sections" do + track_reader = Tracklib::TrackReader.new(data) + expect(track_reader.section_count()).to eq(2) + + expect(track_reader.section_encoding(0)).to eq(:standard) + expect(track_reader.section_schema(0)).to eq([["i64", :i64], + ["f64:2", :f64, 2], + ["u64", :u64], + ["bool", :bool], + ["string", :string], + ["bool array", :bool_array], + ["u64 array", :u64_array], + ["byte array", :byte_array]]) + expect(track_reader.section_rows(0)).to eq(5) + expect(track_reader.section_data(0)).to eq([{"bool" => true, + "bool array" => [true], + "byte array" => [12, 10, 13].pack("C*"), + "f64:2" => 0.78, + "i64" => 42, + "string" => "hey", + "u64" => 25, + "u64 array" => [12, 10, 13]}, + {"bool" => true, + "bool array" => [true], + "byte array" => [12, 10, 13].pack("C*"), + "f64:2" => 0.78, + "i64" => 42, + "string" => "hey", + "u64" => 25, + "u64 array" => [12, 10, 13]}, + {"bool" => true, + "bool array" => [true], + "byte array" => [12, 10, 13].pack("C*"), + "f64:2" => 0.78, + "i64" => 42, + "string" => "hey", + "u64" => 25, + "u64 array" => [12, 10, 13]}, + {"bool" => true, + "bool array" => [true], + "byte array" => [12, 10, 13].pack("C*"), + "f64:2" => 0.78, + "i64" => 42, + "string" => "hey", + "u64" => 25, + "u64 array" => [12, 10, 13]}, + {"bool" => true, + "bool array" => [true], + "byte array" => [12, 10, 13].pack("C*"), + "f64:2" => 0.78, + "i64" => 42, + "string" => "hey", + "u64" => 25, + "u64 array" => [12, 10, 13]}]) + + expect(track_reader.section_encoding(1)).to eq(:standard) + expect(track_reader.section_schema(1)).to eq([["a", :i64], ["b", :bool], ["c", :string]]) + expect(track_reader.section_rows(1)).to eq(3) + expect(track_reader.section_data(1)).to eq([{"a"=>1, "b"=>false, "c"=>"Ride"}, + {"a"=>2, "c"=>"with"}, + {"a"=>4, "b"=>true, "c"=>"GPS"}]) + end + + it "raises an exception for an invalid section index" do + track_reader = Tracklib::TrackReader.new(data) + expect { track_reader.section_encoding(2) }.to raise_error("Section does not exist") + end + + it "can select a subset of fields" do + track_reader = Tracklib::TrackReader.new(data) + # field that doesn't exist + expect(track_reader.section_column(0, "missing column")).to eq(nil) + + # field that does exist + expect(track_reader.section_column(0, "i64")).to eq([42, 42, 42, 42, 42]) + + # missing values are nil + expect(track_reader.section_column(1, "b")).to eq([false, nil, true]) + + # invalid field type + expect { track_reader.section_column(0, :foo) }.to raise_error("Error converting to String") + + # section doesn't exist + expect { track_reader.section_column(2, "a") }.to raise_error("Section does not exist") + end +end diff --git a/ruby_tracklib/spec/roundtrip_spec.rb b/ruby_tracklib/spec/roundtrip_spec.rb new file mode 100644 index 0000000..30c782b --- /dev/null +++ b/ruby_tracklib/spec/roundtrip_spec.rb @@ -0,0 +1,207 @@ +require "spec_helper" + +describe Tracklib do + it "can roundtrip an I64 column" do + data = [{"a" => 0}, + {}, + {}, + {"a" => 40}, + {"a" => -40.0}] + + schema = Tracklib::Schema.new([["a", :i64]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip an F64 column" do + scale = 7 + i64_max = 2 ** 63 + schema_max = i64_max / 10 ** scale + + data = [{"a" => 0}, + {}, + {"a" => 11.2}, + {"a" => -400.000003}, + {"a" => -schema_max}, + {"a" => schema_max}, + {"a" => 50.1234567}] + + schema = Tracklib::Schema.new([["a", :f64, scale]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip an U64 column" do + data = [{"a" => 0}, + {}, + {"a" => 11}, + {"a" => 400}, + {"a" => 20}] + + schema = Tracklib::Schema.new([["a", :u64]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip a Bool column" do + data = [{"a" => false}, + {}, + {"a" => true}, + {"a" => true}, + {}] + + schema = Tracklib::Schema.new([["a", :bool]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip a String column" do + data = [{"a" => "RWGPS"}, + {}, + {"a" => "Supercalifragilisticexpialidocious"}, + {"a" => ""}] + + schema = Tracklib::Schema.new([["a", :string]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip a BoolArray column" do + data = [{"a" => [true, false]}, + {}, + {"a" => [false, false, false, false, false, false, false, false, false, false, false]}, + {"a" => []}] + + schema = Tracklib::Schema.new([["a", :bool_array]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip a U64Array column" do + data = [{"a" => [0, 20, 1, 5_000]}, + {}, + {"a" => []}, + {"a" => [80_000_000, 5]}] + + schema = Tracklib::Schema.new([["a", :u64_array]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip a ByteArray column" do + example_string = [0, 65].pack("C*") + expect(example_string.encoding()). to eq(Encoding::find("ASCII-8BIT")) + data = [{"a" => "RWGPS"}, + {}, + {"a" => ""}, + {"a" => example_string}] + + schema = Tracklib::Schema.new([["a", :byte_array]]) + section = Tracklib::Section::standard(schema, data) + buf = Tracklib::write_track([], [section]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.section_data(0)).to eq(data) + end + + it "can roundtrip all types and metadata" do + metadata = [[:created_at, Time.new(1970, 1, 2, 11, 12, 13, "UTC")], + [:track_type, :route, 1000]] + + schema = [["i64", :i64], + ["f64", :f64, 2], + ["u64", :u64], + ["bool", :bool], + ["string", :string], + ["boolarray", :bool_array], + ["u64array", :u64_array], + ["bytearray", :byte_array]] + + data0 = [{"i64" => -200, + "f64" => 37.89, + "u64" => 80_000_000_000, + "bool" => true, + "string" => "RWGPS", + "boolarray" => [false, true, false], + "u64array" => [20, 10, 11], + "bytearray" => "RWGPS"}] + section0 = Tracklib::Section::standard(Tracklib::Schema.new(schema), data0) + + data1 = [{"i64" => 11, + "bool" => false, + "string" => "Hello"}, + {"f64" => 21.12, + "u64" => 2000, + "boolarray" => [], + "u64array" => [], + "bytearray" => ""}] + section1 = Tracklib::Section::encrypted(Tracklib::Schema.new(schema), data1, "01234567890123456789012345678901") + + buf = Tracklib::write_track(metadata, [section0, section1]) + reader = Tracklib::TrackReader::new(buf) + expect(reader.metadata()).to eq(metadata) + + # Check section 0 - standard encoding + + expect(reader.section_schema(0)).to eq(schema) + expect(reader.section_rows(0)).to eq(1) + expect(reader.section_encoding(0)).to eq(:standard) + expect(reader.section_data(0)).to eq(data0) + expect(reader.section_column(0, "i64")).to eq([-200]) + + # the password argument is ignored for a Standard section + expect(reader.section_data(0, "01234567890123456789012345678901")).to eq(data0) + expect(reader.section_data(0, nil)).to eq(data0) + expect(reader.section_data(0, "Invalid Password")).to eq(data0) + expect(reader.section_column(0, "i64", "Invalid Password")).to eq([-200]) + + + # Check section 1 - encrypted encoding + + expect(reader.section_schema(1)).to eq(schema) + expect(reader.section_rows(1)).to eq(2) + expect(reader.section_encoding(1)).to eq(:encrypted) + expect(reader.section_data(1, "01234567890123456789012345678901")).to eq(data1) + expect(reader.section_column(1, "i64", "01234567890123456789012345678901")).to eq([11, nil]) + expect(reader.section_column(1, "f64", "01234567890123456789012345678901")).to eq([nil, 21.12]) + + # only the right password works when it's encrypted + expect { reader.section_data(1, nil) }.to raise_error + expect { reader.section_data(1, "Invalid Password") }.to raise_error + expect { reader.section_data(1, "00004567890123456789012345678901") }.to raise_error + expect { reader.section_column(1, "i64", "00004567890123456789012345678901") }.to raise_error + end + + it "will trim schema fields" do + schema = Tracklib::Schema.new([["a", :string], + ["b", :bool], + ["c", :u64]]) + data = [{"a" => "RWGPS"}, + {}, + {"a" => "", "c" => 0}] + key = "01234567890123456789012345678901" + standard_section = Tracklib::Section::standard(schema, data) + encrypted_section = Tracklib::Section::encrypted(schema, data, key) + buf = Tracklib::write_track([], [standard_section, encrypted_section]) + reader = Tracklib::TrackReader::new(buf) + + expect(reader.section_data(0)).to eq(data) + expect(reader.section_schema(0)).to eq([["a", :string], ["c", :u64]]) + + expect(reader.section_data(1, key)).to eq(data) + expect(reader.section_schema(1)).to eq([["a", :string], ["c", :u64]]) + end +end diff --git a/ruby_tracklib/spec/spec_helper.rb b/ruby_tracklib/spec/spec_helper.rb index 9c1e978..bed7221 100644 --- a/ruby_tracklib/spec/spec_helper.rb +++ b/ruby_tracklib/spec/spec_helper.rb @@ -4,5 +4,5 @@ require 'tracklib' RSpec::configure do |config| - + RSpec::Expectations.configuration.on_potential_false_positives = :nothing end diff --git a/ruby_tracklib/spec/writer_spec.rb b/ruby_tracklib/spec/writer_spec.rb new file mode 100644 index 0000000..5341bf0 --- /dev/null +++ b/ruby_tracklib/spec/writer_spec.rb @@ -0,0 +1,789 @@ +require "spec_helper" + +describe Tracklib do + it "can write an I64 column" do + schema = Tracklib::Schema.new([["a", :i64]]) + section = Tracklib::Section::standard(schema, [{"a" => 0}, + {}, + {"a" => 40}, + {"a" => -40}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x04, # point count + 0x10, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x00, # field type = I64 + 0x01, # field name length + 0x61, # field name = "a" + 0x08, # leb128 data size + + # Data Table CRC + 0x8A, + 0x59, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0b00000001, + 0x58, # crc + 0x64, + 0x4E, + 0x32, + + # Data Column 1 = "a" + 0x00, # 0 + 0x28, # 40 + 0xB0, # -40 + 0x7F, + 0xAB, # crc + 0x03, + 0xAE, + 0x67]) + end + + it "can write an F64 column" do + schema = Tracklib::Schema.new([["a", :f64, 7]]) + section = Tracklib::Section::standard(schema, [{"a" => 0.0003}, + {}, + {"a" => -27.2}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x03, # point count + 0x12, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x01, # field type = F64 + 0x07, # scale + 0x01, # field name length + 0x61, # field name = "a" + 0x0B, # leb128 data size + + # Data Table CRC + 0x6D, + 0x30, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0xCF, # crc + 0x33, + 0x82, + 0x4D, + + # Data Column 1 = "a" + 0xB7, # first val + 0x17, + 0xC9, # second val + 0xA0, + 0xA6, + 0xFE, + 0x7E, + + 0xAF, # crc + 0x4E, + 0x38, + 0xBE]) + end + + it "can write an U64 column" do + schema = Tracklib::Schema.new([["a", :u64]]) + section = Tracklib::Section::standard(schema, [{"a" => 0}, + {}, + {"a" => 40}, + {"a" => 10}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x04, # point count + 0x0F, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x02, # field type = U64 + 0x01, # field name length + 0x61, # field name = "a" + 0x07, # leb128 data size + + # Data Table CRC + 0x25, + 0x24, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0b00000001, + 0x58, # crc + 0x64, + 0x4E, + 0x32, + + # Data Column 1 = "a" + 0x00, # 0 + 0x28, # 40 + 0x62, # -30 + 0x72, # crc + 0x0A, + 0x57, + 0x47]) + end + + it "can write a Bool column" do + schema = Tracklib::Schema.new([["a", :bool]]) + section = Tracklib::Section::standard(schema, [{"a" => true}, + {}, + {"a" => false}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x03, # point count + 0x0D, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x10, # field type = Bool + 0x01, # field name length + 0x61, # field name = "a" + 0x06, # leb128 data size + + # Data Table CRC + 0x83, + 0xBA, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0xCF, # crc + 0x33, + 0x82, + 0x4D, + + # Data Column 1 = "a" + 0x01, # true + 0x00, # false + 0x5E, # crc + 0x5A, + 0x51, + 0x2D]) + end + + it "can write a String column" do + schema = Tracklib::Schema.new([["a", :string]]) + section = Tracklib::Section::standard(schema, [{"a" => "RWGPS"}, + {}, + {"a" => "Supercalifragilisticexpialidocious"}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x03, # point count + 0x34, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x20, # field type = String + 0x01, # field name length + 0x61, # field name = "a" + 0x2D, # leb128 data size + + # Data Table CRC + 0x65, + 0xA6, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0xCF, # crc + 0x33, + 0x82, + 0x4D, + + # Data Column 1 = "a" + 0x05, # length 5 + 0x52, # R + 0x57, # W + 0x47, # G + 0x50, # P + 0x53, # S + 0x22, # length 34 + 0x53, # S + 0x75, # u + 0x70, # p + 0x65, # e + 0x72, # r + 0x63, # c + 0x61, # a + 0x6C, # l + 0x69, # i + 0x66, # f + 0x72, # r + 0x61, # a + 0x67, # g + 0x69, # i + 0x6C, # l + 0x69, # i + 0x73, # s + 0x74, # t + 0x69, # i + 0x63, # c + 0x65, # e + 0x78, # x + 0x70, # p + 0x69, # i + 0x61, # a + 0x6C, # l + 0x69, # i + 0x64, # d + 0x6F, # o + 0x63, # c + 0x69, # i + 0x6F, # o + 0x75, # u + 0x73, # s + 0xC2, # crc + 0x88, + 0x97, + 0xF3]) + end + + it "can write a BoolArray column" do + schema = Tracklib::Schema.new([["a", :bool_array]]) + section = Tracklib::Section::standard(schema, [{"a" => [true, false, false]}, + {}, + {"a" => []}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x03, # point count + 0x10, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x21, # field type = BoolArray + 0x01, # field name length + 0x61, # field name = "a" + 0x09, # leb128 data size + + # Data Table CRC + 0x00, + 0x43, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0xCF, # crc + 0x33, + 0x82, + 0x4D, + + # Data Column 1 = "a" + 0x03, # array len 3 + 0x01, # true + 0x00, # false + 0x00, # false + 0x00, # array len 0, + 0x43, + 0x76, + 0x95, + 0xBF]) + end + + it "can write a U64Array column" do + schema = Tracklib::Schema.new([["a", :u64_array]]) + section = Tracklib::Section::standard(schema, [{"a" => [99, 98, 500]}, + {}, + {"a" => []}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x03, # point count + 0x12, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x22, # field type = U64Array + 0x01, # field name length + 0x61, # field name = "a" + 0x0B, # leb128 data size + + # Data Table CRC + 0xA2, + 0x06, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0xCF, # crc + 0x33, + 0x82, + 0x4D, + + # Data Column 1 = "a" + 0x03, # array len 3 + 0xE3, # 99 + 0x00, + 0x7F, # -1 + 0x92, # 98 + 0x03, + 0x00, # array len 0 + 0xF1, # crc + 0x29, + 0x76, + 0x36]) + end + + it "can write a ByteArray column" do + schema = Tracklib::Schema.new([["a", :byte_array]]) + section = Tracklib::Section::standard(schema, [{"a" => "RWGPS"}, + {}, + {"a" => ""}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x03, # point count + 0x12, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x23, # field type = ByteArray + 0x01, # field name length + 0x61, # field name = "a" + 0x0B, # leb128 data size + + # Data Table CRC + 0xA3, + 0xFA, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000001, + 0xCF, # crc + 0x33, + 0x82, + 0x4D, + + # Data Column 1 = "a" + 0x05, # array len 5 + 0x52, # R + 0x57, # W + 0x47, # G + 0x50, # P + 0x53, # S + 0x00, # array len 0 + 0x6A, # crc + 0x29, + 0x93, + 0xA3]) + end + + it "can convert floats and integers" do + schema = Tracklib::Schema.new([["i", :i64], + ["u", :u64], + ["f", :f64, 7], + ["ua", :u64_array]]) + section = Tracklib::Section::standard(schema, + [{"i" => -1, "u" => 1, "f" => 0.0, "ua" => [0, 1.0, 1.2, 1.9, 5]}, + {"i" => 1.0, "u"=> 3.0, "f" => 5}, + {}, + {"i" => -2.6, "u" => 32.21}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x04, # point count + 0x29, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x04, # field count + 0x00, # first field type = I64 + 0x01, # field name length + 0x69, # field name = "i" + 0x07, # leb128 data size + 0x02, # second field type = U64 + 0x01, # field name length + 0x75, # field name = "u" + 0x07, # leb128 data size + 0x01, # third field type = F64 + 0x07, # scale + 0x01, # field name length + 0x66, # field name = "f" + 0x09, # leb128 data size + 0x22, # fourth field type = U64Array + 0x02, # field name length + 0x75, # field name = "ua" + 0x61, + 0x0A, # leb128 data size + + # Data Table CRC + 0x8D, + 0x59, + + # Data Section 1 + + # Presence Column + 0b00001111, + 0b00000111, + 0b00000000, + 0b00000011, + 0xA9, # crc + 0x25, + 0xDB, + 0xD5, + + # Data Column 1 = "i" + 0x7F, # -1 + 0x02, # +2 + 0x7C, # -4 + 0x8E, # crc + 0xC0, + 0xAB, + 0x66, + + # Data Column 2 = "u" + 0x01, # 1 + 0x02, # +2 + 0x1D, # +29 + 0xD4, # crc + 0xED, + 0x6D, + 0x94, + + # Data Column 3 = "f" + 0x00, + 0x80, + 0xE1, + 0xEB, + 0x17, + 0x5F, # crc + 0x32, + 0x25, + 0xF7, + + # Data Column 4 = "ua" + 0x05, # array len + 0x00, # 0 + 0x01, # +1 + 0x00, # no change + 0x01, # +1 + 0x03, # +3 + 0xA7, # crc + 0x06, + 0x3A, + 0xB1]) + end + + it "raises errors for invalid F64 scale" do + expect { Tracklib::Schema.new([["a", :f64]]) }.to raise_error + expect { Tracklib::Schema.new([["a", :f64, "2"]]) }.to raise_error + expect { Tracklib::Schema.new([["a", :f64, 500]]) }.to raise_error + end + + it "raises errors for floats too big to fit in the configured F64" do + scale = 7 + schema = Tracklib::Schema.new([["f", :f64, scale]]) + i64_max = 2 ** 63 + schema_max = i64_max / 10 ** scale + expect { Tracklib::Section::standard(schema, [{"f" => schema_max + 1}]) }.to raise_error + expect { Tracklib::Section::standard(schema, [{"f" => -schema_max - 1}]) }.to raise_error + section = Tracklib::Section::standard(schema, [{"f" => schema_max }]) + + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x01, # point count + 0x13, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x01, # field count + 0x01, # first field type = F64 + 0x07, # scale + 0x01, # field name length + 0x66, # field name = "f" + 0x0E, # leb128 data size + + # Data Table CRC + 0x77, + 0xAF, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0xFC, # crc + 0x5D, + 0x36, + 0xB5, + + # Data Column 1 = "f" + 0x80, # schema max + 0xC0, + 0xDC, + 0xFD, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x67, # crc + 0x62, + 0x37, + 0x83]) + end + + it "raises errors for invalid array type elements" do + expect { + Tracklib::Section::standard(Tracklib::Schema.new([["a", :bool_array]]), + [{"a" => [true, false, 0]}]) + }.to raise_error + + expect { + Tracklib::Section::standard(Tracklib::Schema.new([["a", :u64_array]]), + [{"a" => [0, 1, 2, "3"]}]) + }.to raise_error + end + + it "can write metadata" do + # empty metadata + expect(Tracklib::write_track([], []) + .unpack("C*")[24..]) + .to eq([0x00, # empty metadata table + 0x40, + 0xBF, + + 0x00, # empty data table + 0x40, + 0xBF]) + + # track_type + expect(Tracklib::write_track([[:track_type, :route, 64]], []) + .unpack("C*")[24..]) + .to eq([0x01, # metadata table len + 0x00, # entry type: track_type + 0x02, # entry size + 0x01, # track type: route + 0x40, # route id + 0x47, # crc + 0x9F, + + 0x00, # empty data table + 0x40, + 0xBF]) + + # created_at + expect(Tracklib::write_track([[:created_at, Time.new(1970, 1, 1, 0, 0, 0, "UTC")]], []) + .unpack("C*")[24..]) + .to eq([0x01, # metadata table len = 1 + 0x01, # entry type: created_at + 0x01, # entry size + 0x00, # timestamp + 0xAE, # crc + 0x77, + + 0x00, # empty data table + 0x40, + 0xBF]) + + # both + expect(Tracklib::write_track([[:track_type, :trip, 20], + [:created_at, Time.new(1970, 1, 1, 0, 0, 0, "UTC")]], + []) + .unpack("C*")[24..]) + .to eq([0x02, # two metadata entries + 0x00, # entry type: track_type + 0x02, # entry size + 0x00, # track type: trip + 0x14, # trip id + 0x01, # entry type: created_at + 0x01, # entry size + 0x00, # timestamp + 0x6A, # crc + 0x6F, + + 0x00, # empty data table + 0x40, + 0xBF]) + + # unknown type + expect { Tracklib::write_track([[:foo, 25]], []) } .to raise_error "Metadata Type 'foo' unknown" + + # invalid length + expect { Tracklib::write_track([[:created_at, Time.now, "foo"]], []) } .to raise_error "Metadata Entries for 'created_at' must have length 2" + + # invalid args + expect { Tracklib::write_track([[]], []) } .to raise_error "Invalid Metadata Entry" + expect { Tracklib::write_track(["Foo"], []) } .to raise_error "Error converting to Array" + expect { Tracklib::write_track([[:created_at, "foo"]], []) } .to raise_error "Error converting to Time" + expect { Tracklib::write_track([[:track_type, :foo, 5]], []) } .to raise_error "Metadata Entry Track Type 'foo' unknown" + expect { Tracklib::write_track([[:track_type, :route, "foo"]], []) } .to raise_error "Error converting to Integer" + end + + it "raises errors if data doesn't match the schema" do + expect { Tracklib::Section::standard(Tracklib::Schema.new([["a", :string], + ["b", :bool], + ["c", :u64]]), + [{"a" => "RWGPS", + "d" => "RWGPS"}]) } .to raise_error "Schema is missing field(s)" + expect { Tracklib::Section::standard(Tracklib::Schema.new([["a", :string], + ["b", :bool], + ["c", :u64]]), + [{"d" => "RWGPS"}]) } .to raise_error "Schema is missing field(s)" + expect { Tracklib::Section::standard(Tracklib::Schema.new([["a", :string], + ["b", :bool], + ["c", :u64]]), + [{"a" => "RWGPS", "b" => false, "c" => 0, "d" => "RWGPS"}]) + } .to raise_error "Schema is missing field(s)" + end + + it "trims schema to only store fields in use" do + schema = Tracklib::Schema.new([["a", :string], + ["b", :bool], + ["c", :u64]]) + section = Tracklib::Section::standard(schema, [{"a" => "RWGPS"}, + {}, + {"a" => "", "c" => 0}]) + expect(Tracklib::write_track([], [section]) + .unpack("C*")[27..]) + .to eq([# Data Table + + 0x01, # one data section + + # Data Table Section 1 + 0x00, # data encoding = standard + 0x03, # point count + 0x17, # leb128 data size + + # Schema for Section 1 + 0x00, # schema version + 0x02, # field count + 0x20, # field type = String + 0x01, # field name length + 0x61, # field name = "a" + 0x0B, # leb128 data size + 0x02, # field type = U64 + 0x01, # field name length + 0x63, # field name = "c" + 0x05, # leb128 data size + + # Data Table CRC + 0x00, + 0x0C, + + # Data Section 1 + + # Presence Column + 0b00000001, + 0b00000000, + 0b00000011, + 0xA1, # crc + 0x08, + 0x00, + 0x44, + + # Data Column 1 = "a" + 0x05, # array len 5 + 0x52, # R + 0x57, # W + 0x47, # G + 0x50, # P + 0x53, # S + 0x00, # array len 0 + 0x6A, # crc + 0x29, + 0x93, + 0xA3, + + # Data Column 2 = "c" + 0x00, # 0 + 0x4B, # crc + 0x40, + 0xF7, + 0xB1]) + end +end diff --git a/ruby_tracklib/src/lib.rs b/ruby_tracklib/src/lib.rs index f7f5569..5cd9cbc 100644 --- a/ruby_tracklib/src/lib.rs +++ b/ruby_tracklib/src/lib.rs @@ -1,394 +1,35 @@ -use std::collections::{HashMap}; -use std::convert::{TryFrom}; -use std::io::{BufWriter}; -use lazy_static::lazy_static; -use rutie::{ - class, methods, wrappable_struct, AnyObject, Array, Boolean, Class, Encoding, Float, Hash, Integer, - Module, Object, RString, VM, -}; -use rutie_serde::{ruby_class, rutie_serde_methods}; -use tracklib::{parse_rwtf, DataField, RWTFile, RWTFMetadata, TrackType}; +pub mod read; +mod schema; +mod write; +use rutie::{Module, Object}; -fn any_to_float(o: AnyObject) -> f64 { - match o.try_convert_to::() { - Ok(f) => f.to_f64(), - Err(float_e) => o - .try_convert_to::() - .map_err(|_| VM::raise_ex(float_e)) - .unwrap() - .to_i64() as f64, - } -} - -fn any_to_int(o: AnyObject) -> i64 { - match o.try_convert_to::() { - Ok(i) => i.to_i64(), - Err(int_e) => o - .try_convert_to::() - .map_err(|_| VM::raise_ex(int_e)) - .unwrap() - .to_f64() as i64, - } -} - -fn any_to_str(o: AnyObject) -> String { - o.try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap() - .to_string() -} - -fn any_to_bool(o: AnyObject) -> bool { - o.try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap() - .to_bool() -} - -fn any_to_ids(o: AnyObject) -> Vec { - o.try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap() - .into_iter() - .map(|ele| { - ele.try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap() - .to_u64() - }) - .collect() -} - -#[derive(Debug, Copy, Clone)] -enum ColumnType { - Numbers, - LongFloat, - ShortFloat, - Base64, - String, - Bool, - IDs, -} - -impl ColumnType { - fn from_str(name: &str) -> Option { - match name { - "Number" => Some(ColumnType::Numbers), - "LongFloat" => Some(ColumnType::LongFloat), - "ShortFloat" => Some(ColumnType::ShortFloat), - "Base64" => Some(ColumnType::Base64), - "String" => Some(ColumnType::String), - "Bool" => Some(ColumnType::Bool), - "IDs" => Some(ColumnType::IDs), - _ => None - } - } - - fn exponent(&self) -> u8 { - match self { - ColumnType::Numbers => 48, - ColumnType::LongFloat => 24, - ColumnType::ShortFloat => 38, - _ => { - VM::raise(Class::from_existing("Exception"), - &format!("can't handle numeric value for non-numeric field")); - unreachable!(); - } - } - } - - fn max_integer(&self) -> i64 { - 2i64.pow(u32::from(self.exponent())) - } - - fn max_float(&self) -> f64 { - 2f64.powi(i32::from(self.exponent())) - } -} - -fn convert_config(config: Hash) -> HashMap { - let mut hm = HashMap::new(); - - config.each(|rwtf_type_name, array_of_field_names| { - let type_name_obj = rwtf_type_name - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - let type_name_str = type_name_obj.to_str(); - - if let Some(column_type) = ColumnType::from_str(type_name_str) { - let array_obj = array_of_field_names - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - - for field_name in array_obj.into_iter() { - let field_name_obj = field_name - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - - hm.insert(field_name_obj.to_string(), column_type); - } - } else { - VM::raise(Class::from_existing("Exception"), - &format!("unknown rwtf_field_config type: {}", type_name_str)); - unreachable!(); - } - }); - - hm -} - -fn is_empty_string(v: &AnyObject) -> bool { - match v.try_convert_to::() { - Ok(s) => s.to_str().is_empty(), - Err(_) => false - } -} - -fn is_empty_array(v: &AnyObject) -> bool { - match v.try_convert_to::() { - Ok(a) => a.length() == 0, - Err(_) => false - } -} - -fn is_number_and_too_large(v: &AnyObject, field_type: ColumnType) -> bool { - // First we have to try to cast `v` to a numeric type (Integer or Float) - // and then call to_i64/f64(). This will raise an exception if the number - // is too large to turn into a primitive. - let is_number_result = VM::protect(|| { - match v.try_convert_to::() { - Ok(i) => { - let _ = i.to_i64(); // force a conversion - Boolean::new(true) - } - Err(_) => match v.try_convert_to::() { - Ok(f) => { - let _ = f.to_f64(); // force a conversion - Boolean::new(true) - } - Err(_) => Boolean::new(false) - } - }.to_any_object() - }); - - match is_number_result { - Ok(is_number) => { - // Here we know that no exception was raised during the attempted primitive conversion. - // We also know that `is_number` is a Boolean, so this unsafe cast is fine. - if unsafe { is_number.to::().to_bool() } { - match v.try_convert_to::() { - Ok(i) => i.to_i64().abs() > field_type.max_integer(), - Err(_) => match v.try_convert_to::() { - Ok(f) => f.to_f64().abs() > field_type.max_float(), - Err(_) => false - } - } - } else { - false - } - } - Err(_) => { - VM::clear_error_info(); // clear ruby VM error register - true // this IS a number and it IS too large - } - } -} - -fn add_points(source: &Hash, - section_points_config: &HashMap, - section_type: &str, - mut callback: impl FnMut(usize, &str, DataField)) { - let maybe_section_points = source - .at(&RString::new_utf8(section_type)) - .try_convert_to::(); - - if let Ok(section_points) = maybe_section_points { - for (i, maybe_point) in section_points.into_iter().enumerate() { - let point = maybe_point - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - - point.each(|k: AnyObject, v: AnyObject| { - let field_name_obj = k - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - let name = field_name_obj.to_str(); - - if !name.is_empty() { - if let Some(field_type) = section_points_config.get(name) { - if !v.is_nil() - && !is_empty_string(&v) - && !is_empty_array(&v) - && !is_number_and_too_large(&v, *field_type) - { - let data = match field_type { - ColumnType::LongFloat => DataField::LongFloat(any_to_float(v)), - ColumnType::ShortFloat => DataField::ShortFloat(any_to_float(v)), - ColumnType::Numbers => DataField::Number(any_to_int(v)), - ColumnType::Base64 => DataField::Base64(any_to_str(v).replace("\n", "")), - ColumnType::String => DataField::String(any_to_str(v)), - ColumnType::Bool => DataField::Bool(any_to_bool(v)), - ColumnType::IDs => DataField::IDs(any_to_ids(v)), - }; - - callback(i, name, data); - } - } else { - VM::raise(Module::from_existing("Tracklib").get_nested_class("UnknownFieldError"), - &format!("unknown {} field: {}", section_type, name)); - unreachable!(); - } - } - }); - } - } -} - -pub struct Inner { - inner: RWTFile, -} - -wrappable_struct!(Inner, InnerWrapper, INNER_WRAPPER); - -class!(RubyRWTFile); - -methods!( - RubyRWTFile, - itself, - - fn rwtf_from_bytes(bytes: RString) -> AnyObject { - let source = bytes.map_err(|e| VM::raise_ex(e)).unwrap(); - let (_, rwtf) = parse_rwtf(source.to_bytes_unchecked()) - .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) - .unwrap(); - let inner = Inner { inner: rwtf }; - - Class::from_existing("RWTFile").wrap_data(inner, &*INNER_WRAPPER) - } - - fn rwtf_to_bytes() -> RString { - let mut writer = BufWriter::new(Vec::new()); - itself.get_data(&*INNER_WRAPPER).inner.write(&mut writer) - .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) - .unwrap(); - - let encoding = Encoding::find("ASCII-8BIT") - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - - let buf = writer.into_inner() - .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) - .unwrap(); - - RString::from_bytes(&buf, &encoding) - } - - fn rwtf_from_hash(input: Hash, config_input: Hash, metadata: Hash) -> AnyObject { - let source = input.map_err(|e| VM::raise_ex(e)).unwrap(); - let config = config_input.map_err(|e| VM::raise_ex(e)).unwrap(); - let track_points_config = convert_config(config.at(&RString::new_utf8("track_points")) - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap()); - let course_points_config = convert_config(config.at(&RString::new_utf8("course_points")) - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap()); - - let mut rwtf = if let Some(md) = metadata.ok() { - let tt_metadata = md.at(&RString::new_utf8("track_type")) - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - - let track_type = tt_metadata.at(&RString::new_utf8("type")) - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap(); - - let id = u32::try_from(tt_metadata.at(&RString::new_utf8("id")) - .try_convert_to::() - .map_err(|e| VM::raise_ex(e)) - .unwrap() - .to_u64()) - .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) - .unwrap(); - - let tt = match track_type.to_str() { - "trip" => TrackType::Trip(id), - "route" => TrackType::Route(id), - "segment" => TrackType::Segment(id), - _ => { - VM::raise( - Class::from_existing("Exception"), - &format!("unknown track_type metadata: {}", track_type.to_str()), - ); - unreachable!(); - } - }; - - RWTFile::with_track_type(tt) - } else { - RWTFile::new() - }; - - add_points(&source, &track_points_config, "track_points", |i, name, data| { - rwtf.add_track_point(i, name, data) - .map_err(|e| { - VM::raise(Class::from_existing("Exception"), &format!("{}", e)) - }) - .unwrap(); +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn Init_Tracklib() { + Module::from_existing("Tracklib").define(|module| { + module.define_nested_class("TrackReader", None).define(|class| { + class.def_self("new", read::trackreader_new); + class.def("metadata", read::trackreader_metadata); + class.def("file_version", read::trackreader_file_version); + class.def("creator_version", read::trackreader_creator_version); + + class.def("section_count", read::trackreader_section_count); + class.def("section_encoding", read::trackreader_section_encoding); + class.def("section_schema", read::trackreader_section_schema); + class.def("section_rows", read::trackreader_section_rows); + class.def("section_data", read::trackreader_section_data); + class.def("section_column", read::trackreader_section_column); }); - add_points(&source, &course_points_config, "course_points", |i, name, data| { - rwtf.add_course_point(i, name, data) - .map_err(|e| { - VM::raise(Class::from_existing("Exception"), &format!("{}", e)) - }) - .unwrap(); + module.define_nested_class("Schema", None).define(|class| { + class.def_self("new", schema::schema_new); }); - let inner = Inner { inner: rwtf }; - Class::from_existing("RWTFile").wrap_data(inner, &*INNER_WRAPPER) - } - - fn rwtf_inspect() -> RString { - let rwtf = &itself.get_data(&*INNER_WRAPPER).inner; - - RString::new_utf8(&format!("RWTFile", - rwtf.track_points.len(), - rwtf.course_points.len())) - } -); - -rutie_serde_methods!( - RubyRWTFile, - itself, - ruby_class!(Exception), - - fn rwtf_to_hash() -> &RWTFile { - &itself.get_data(&*INNER_WRAPPER).inner - } - - fn rwtf_metadata() -> &RWTFMetadata { - &itself.get_data(&*INNER_WRAPPER).inner.metadata() - } -); + module.define_nested_class("Section", None).define(|class| { + class.def_self("standard", write::section_standard); + class.def_self("encrypted", write::section_encrypted); + }); -#[allow(non_snake_case)] -#[no_mangle] -pub extern "C" fn Init_Tracklib() { - Class::new("RWTFile", Some(&Class::from_existing("Object"))).define(|itself| { - itself.def_self("from_bytes", rwtf_from_bytes); - itself.def_self("from_h", rwtf_from_hash); - itself.def("to_bytes", rwtf_to_bytes); - itself.def("to_h", rwtf_to_hash); - itself.def("metadata", rwtf_metadata); - itself.def("inspect", rwtf_inspect); + module.define_module_function("write_track", write::write_track); }); } diff --git a/ruby_tracklib/src/read.rs b/ruby_tracklib/src/read.rs new file mode 100644 index 0000000..52049f7 --- /dev/null +++ b/ruby_tracklib/src/read.rs @@ -0,0 +1,376 @@ +use ouroboros::self_referencing; +use rutie::{ + class, methods, wrappable_struct, AnyObject, Array, Boolean, Class, Encoding, Float, Hash, Integer, Module, + NilClass, Object, RString, Symbol, VerifiedObject, VM, +}; +use tracklib::read::section::SectionRead; + +#[self_referencing] +pub struct WrappableTrackReader { + data: Vec, + #[borrows(data)] + #[not_covariant] + track_reader: tracklib::read::track::TrackReader<'this>, +} + +wrappable_struct!(WrappableTrackReader, TrackReaderWrapper, TRACK_READER_WRAPPER_INSTANCE); + +class!(TrackReader); + +methods!( + TrackReader, + rtself, + fn trackreader_new(bytes: RString) -> AnyObject { + let source = bytes.map_err(VM::raise_ex).unwrap(); + let data = source.to_bytes_unchecked().to_vec(); + let wrapper = WrappableTrackReader::new(data, |d| { + tracklib::read::track::TrackReader::new(d) + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap() + }); + + Module::from_existing("Tracklib") + .get_nested_class("TrackReader") + .wrap_data(wrapper, &*TRACK_READER_WRAPPER_INSTANCE) + }, + fn trackreader_metadata() -> Array { + let metadata_entries = rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| track_reader.metadata()); + + let mut metadata_array = Array::new(); + + for metadata_entry in metadata_entries { + let metadata_entry_array = match metadata_entry { + tracklib::types::MetadataEntry::TrackType(track_type) => { + let mut metadata_entry_array = Array::new(); + + let (type_name, id) = match track_type { + tracklib::types::TrackType::Trip(id) => (Symbol::new("trip"), Integer::from(*id)), + tracklib::types::TrackType::Route(id) => (Symbol::new("route"), Integer::from(*id)), + tracklib::types::TrackType::Segment(id) => (Symbol::new("segment"), Integer::from(*id)), + }; + + metadata_entry_array.push(Symbol::new("track_type")); + metadata_entry_array.push(type_name); + metadata_entry_array.push(id); + + metadata_entry_array + } + tracklib::types::MetadataEntry::CreatedAt(created_at) => { + let mut metadata_entry_array = Array::new(); + + metadata_entry_array.push(Symbol::new("created_at")); + + let time_obj = Class::from_existing("Time") + .protect_send("at", &[Integer::from(*created_at).to_any_object()]) + .map_err(VM::raise_ex) + .unwrap() + .protect_send("utc", &[]) + .map_err(VM::raise_ex) + .unwrap(); + + metadata_entry_array.push(time_obj); + + metadata_entry_array + } + }; + + metadata_array.push(metadata_entry_array); + } + + metadata_array + }, + fn trackreader_file_version() -> Integer { + Integer::from(u32::from( + rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| track_reader.file_version()), + )) + }, + fn trackreader_creator_version() -> Integer { + Integer::from(u32::from( + rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| track_reader.creator_version()), + )) + }, + fn trackreader_section_count() -> Integer { + Integer::from( + u64::try_from( + rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| track_reader.section_count()), + ) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "u64 != usize")) + .unwrap(), + ) + }, + fn trackreader_section_encoding(index: Integer) -> Symbol { + let ruby_index = index.map_err(VM::raise_ex).unwrap(); + let rust_index = usize::try_from(ruby_index.to_u64()) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "u64 != usize")) + .unwrap(); + let encoding = rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| { + track_reader.section(rust_index).map(|section| match section { + tracklib::read::section::Section::Standard(section) => section.encoding(), + tracklib::read::section::Section::Encrypted(section) => section.encoding(), + }) + }) + .ok_or_else(|| VM::raise(Class::from_existing("Exception"), "Section does not exist")) + .unwrap(); + + Symbol::new(match encoding { + tracklib::types::SectionEncoding::Standard => "standard", + tracklib::types::SectionEncoding::Encrypted => "encrypted", + }) + }, + fn trackreader_section_schema(index: Integer) -> Array { + let ruby_index = index.map_err(VM::raise_ex).unwrap(); + let rust_index = usize::try_from(ruby_index.to_u64()) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "u64 != usize")) + .unwrap(); + let schema = rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| { + track_reader.section(rust_index).map(|section| match section { + tracklib::read::section::Section::Standard(section) => section.schema(), + tracklib::read::section::Section::Encrypted(section) => section.schema(), + }) + }) + .ok_or_else(|| VM::raise(Class::from_existing("Exception"), "Section does not exist")) + .unwrap(); + + let mut schema_array = Array::new(); + + for field_def in schema.fields() { + let mut field_array = Array::new(); + field_array.push(RString::from(String::from(field_def.name()))); + match field_def.data_type() { + tracklib::schema::DataType::I64 => { + field_array.push(Symbol::new("i64")); + } + tracklib::schema::DataType::F64 { scale } => { + field_array.push(Symbol::new("f64")); + field_array.push(Integer::from(u32::from(*scale))); + } + tracklib::schema::DataType::U64 => { + field_array.push(Symbol::new("u64")); + } + tracklib::schema::DataType::Bool => { + field_array.push(Symbol::new("bool")); + } + tracklib::schema::DataType::String => { + field_array.push(Symbol::new("string")); + } + tracklib::schema::DataType::BoolArray => { + field_array.push(Symbol::new("bool_array")); + } + tracklib::schema::DataType::U64Array => { + field_array.push(Symbol::new("u64_array")); + } + tracklib::schema::DataType::ByteArray => { + field_array.push(Symbol::new("byte_array")); + } + }; + schema_array.push(field_array); + } + + schema_array + }, + fn trackreader_section_rows(index: Integer) -> Integer { + let ruby_index = index.map_err(VM::raise_ex).unwrap(); + let rust_index = usize::try_from(ruby_index.to_u64()) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "u64 != usize")) + .unwrap(); + let rows = rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| { + track_reader.section(rust_index).map(|section| match section { + tracklib::read::section::Section::Standard(section) => section.rows(), + tracklib::read::section::Section::Encrypted(section) => section.rows(), + }) + }) + .ok_or_else(|| VM::raise(Class::from_existing("Exception"), "Section does not exist")) + .unwrap(); + + Integer::from( + u64::try_from(rows) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "u64 != usize")) + .unwrap(), + ) + }, + fn trackreader_section_data(index: Integer, key_material: RString) -> Array { + let ruby_index = index.map_err(VM::raise_ex).unwrap(); + let rust_index = usize::try_from(ruby_index.to_u64()) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "u64 != usize")) + .unwrap(); + let data = rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| { + track_reader.section(rust_index).map(|section| match section { + tracklib::read::section::Section::Standard(section) => reader_to_array_of_hashes( + section + .reader() + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap(), + ), + tracklib::read::section::Section::Encrypted(mut section) => { + let ruby_key_material = key_material.map_err(VM::raise_ex).unwrap(); + let rust_key_material = ruby_key_material.to_bytes_unchecked(); + reader_to_array_of_hashes( + section + .reader(rust_key_material) + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap(), + ) + } + }) + }) + .ok_or_else(|| VM::raise(Class::from_existing("Exception"), "Section does not exist")) + .unwrap(); + + data + }, + fn trackreader_section_column(index: Integer, column_name: RString, key_material: RString) -> AnyObject { + let ruby_index = index.map_err(VM::raise_ex).unwrap(); + let rust_index = usize::try_from(ruby_index.to_u64()) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "u64 != usize")) + .unwrap(); + let data = rtself + .get_data(&*TRACK_READER_WRAPPER_INSTANCE) + .with_track_reader(|track_reader| { + track_reader.section(rust_index).map(|section| { + let ruby_field_name = column_name.map_err(VM::raise_ex).unwrap(); + let field_name = ruby_field_name.to_str(); + + let schema = match section { + tracklib::read::section::Section::Standard(ref section) => section.schema(), + tracklib::read::section::Section::Encrypted(ref section) => section.schema(), + }; + let maybe_field_def = schema.fields().iter().find(|field_def| field_def.name() == field_name); + + if let Some(field_def) = maybe_field_def { + let schema = tracklib::schema::Schema::with_fields(vec![field_def.clone()]); + match section { + tracklib::read::section::Section::Standard(section) => reader_to_single_column_array( + section + .reader_for_schema(&schema) + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap(), + ) + .to_any_object(), + tracklib::read::section::Section::Encrypted(mut section) => { + let ruby_key_material = key_material.map_err(VM::raise_ex).unwrap(); + let rust_key_material = ruby_key_material.to_bytes_unchecked(); + reader_to_single_column_array( + section + .reader_for_schema(rust_key_material, &schema) + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap(), + ) + .to_any_object() + } + } + } else { + NilClass::new().to_any_object() + } + }) + }) + .ok_or_else(|| VM::raise(Class::from_existing("Exception"), "Section does not exist")) + .unwrap(); + + data + } +); + +impl TrackReader { + pub fn with_track_reader<'outer_borrow, ReturnType>( + &'outer_borrow self, + user: impl for<'this> ::core::ops::FnOnce(&'outer_borrow tracklib::read::track::TrackReader<'this>) -> ReturnType, + ) -> ReturnType { + self.get_data(&*TRACK_READER_WRAPPER_INSTANCE).with_track_reader(user) + } +} + +impl VerifiedObject for TrackReader { + fn is_correct_type(object: &T) -> bool { + object.class() == Module::from_existing("Tracklib").get_nested_class("TrackReader") + } + + fn error_message() -> &'static str { + "Error converting to TrackReader" + } +} + +pub fn fieldvalue_to_ruby(value: tracklib::types::FieldValue) -> AnyObject { + match value { + tracklib::types::FieldValue::I64(v) => Integer::new(v).to_any_object(), + tracklib::types::FieldValue::F64(v) => Float::new(v).to_any_object(), + tracklib::types::FieldValue::U64(v) => Integer::from(v).to_any_object(), + tracklib::types::FieldValue::Bool(v) => Boolean::new(v).to_any_object(), + tracklib::types::FieldValue::String(v) => RString::from(v).to_any_object(), + tracklib::types::FieldValue::BoolArray(v) => { + let mut a = Array::new(); + for b in v { + a.push(Boolean::new(b).to_any_object()); + } + a.to_any_object() + } + tracklib::types::FieldValue::U64Array(v) => { + let mut a = Array::new(); + for u in v { + a.push(Integer::from(u).to_any_object()); + } + a.to_any_object() + } + tracklib::types::FieldValue::ByteArray(v) => { + let encoding = Encoding::find("ASCII-8BIT").map_err(VM::raise_ex).unwrap(); + + RString::from_bytes(&v, &encoding).to_any_object() + } + } +} + +fn reader_to_array_of_hashes(mut reader: tracklib::read::section::reader::SectionReader) -> Array { + let mut data_array = Array::new(); + while let Some(columniter) = reader.open_column_iter() { + let mut row_hash = Hash::new(); + for row in columniter { + let (field_def, maybe_value) = row + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap(); + + if let Some(value) = maybe_value { + row_hash.store(RString::from(String::from(field_def.name())), fieldvalue_to_ruby(value)); + } + } + data_array.push(row_hash); + } + + data_array +} + +fn reader_to_single_column_array(mut reader: tracklib::read::section::reader::SectionReader) -> Array { + let mut data_array = Array::new(); + while let Some(mut columniter) = reader.open_column_iter() { + let (_field_def, maybe_value) = columniter + .next() + .ok_or_else(|| VM::raise(Class::from_existing("Exception"), "Missing field inside iterator")) + .unwrap() + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap(); + + let ruby_value = if let Some(value) = maybe_value { + fieldvalue_to_ruby(value) + } else { + NilClass::new().to_any_object() + }; + + data_array.push(ruby_value); + } + + data_array +} diff --git a/ruby_tracklib/src/schema.rs b/ruby_tracklib/src/schema.rs new file mode 100644 index 0000000..3c4780c --- /dev/null +++ b/ruby_tracklib/src/schema.rs @@ -0,0 +1,91 @@ +use rutie::{ + class, methods, wrappable_struct, AnyObject, Array, Class, Integer, Module, Object, RString, Symbol, + VerifiedObject, VM, +}; + +pub struct WrappableSchema { + schema: tracklib::schema::Schema, +} + +wrappable_struct!(WrappableSchema, SchemaWrapper, SCHEMA_WRAPPER_INSTANCE); + +class!(Schema); + +methods!( + Schema, + rtself, + fn schema_new(ruby_schema: Array) -> AnyObject { + let fields = ruby_schema + .map_err(VM::raise_ex) + .unwrap() + .into_iter() + .map(|ele| { + let ruby_schema_entry = ele.try_convert_to::().map_err(VM::raise_ex).unwrap(); + + let ruby_field_name = ruby_schema_entry + .at(0) + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap(); + let ruby_data_type = ruby_schema_entry + .at(1) + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap(); + + let data_type = match ruby_data_type.to_str() { + "i64" => tracklib::schema::DataType::I64, + "f64" => { + let ruby_scale = ruby_schema_entry + .at(2) + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap(); + let scale = u8::try_from(ruby_scale.to_u64()) + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{}", e))) + .unwrap(); + tracklib::schema::DataType::F64 { scale } + } + "u64" => tracklib::schema::DataType::U64, + "bool" => tracklib::schema::DataType::Bool, + "string" => tracklib::schema::DataType::String, + "bool_array" => tracklib::schema::DataType::BoolArray, + "u64_array" => tracklib::schema::DataType::U64Array, + "byte_array" => tracklib::schema::DataType::ByteArray, + val => { + VM::raise( + Class::from_existing("Exception"), + &format!("Schema Data Type '{val}' unknown"), + ); + unreachable!(); + } + }; + + tracklib::schema::FieldDefinition::new(ruby_field_name.to_string(), data_type) + }) + .collect::>(); + + Module::from_existing("Tracklib").get_nested_class("Schema").wrap_data( + WrappableSchema { + schema: tracklib::schema::Schema::with_fields(fields), + }, + &*SCHEMA_WRAPPER_INSTANCE, + ) + }, +); + +impl Schema { + pub(crate) fn inner(&self) -> &tracklib::schema::Schema { + &self.get_data(&*SCHEMA_WRAPPER_INSTANCE).schema + } +} + +impl VerifiedObject for Schema { + fn is_correct_type(object: &T) -> bool { + object.class() == Module::from_existing("Tracklib").get_nested_class("Schema") + } + + fn error_message() -> &'static str { + "Error converting to Schema" + } +} diff --git a/ruby_tracklib/src/write.rs b/ruby_tracklib/src/write.rs new file mode 100644 index 0000000..92e6996 --- /dev/null +++ b/ruby_tracklib/src/write.rs @@ -0,0 +1,411 @@ +use rutie::{ + class, methods, module, wrappable_struct, AnyObject, Array, Boolean, Class, Encoding, Float, Hash, Integer, Module, + Object, RString, Symbol, VerifiedObject, VM, +}; +use std::collections::HashSet; + +pub struct WrappableSection { + section: tracklib::write::section::Section, +} +use tracklib::write::section::SectionWrite; + +wrappable_struct!(WrappableSection, SectionWrapper, SECTION_WRAPPER_INSTANCE); + +class!(Section); + +methods!( + Section, + rtself, + fn section_standard(schema: crate::schema::Schema, data: Array) -> AnyObject { + let ruby_data = data.map_err(VM::raise_ex).unwrap(); + let ruby_schema = schema.map_err(VM::raise_ex).unwrap(); + let base_tracklib_schema = ruby_schema.inner(); + let trimmed_tracklib_schema = trim_schema(base_tracklib_schema, &ruby_data); + let mut tracklib_section = tracklib::write::section::standard::Section::new(trimmed_tracklib_schema); + write_ruby_array_into_section(&mut tracklib_section, ruby_data); + + Module::from_existing("Tracklib").get_nested_class("Section").wrap_data( + WrappableSection { + section: tracklib::write::section::Section::Standard(tracklib_section), + }, + &*SECTION_WRAPPER_INSTANCE, + ) + }, + fn section_encrypted(schema: crate::schema::Schema, data: Array, key_material: RString) -> AnyObject { + let ruby_data = data.map_err(VM::raise_ex).unwrap(); + let ruby_key_material = key_material.map_err(VM::raise_ex).unwrap(); + let rust_key_material = ruby_key_material.to_bytes_unchecked(); + let ruby_schema = schema.map_err(VM::raise_ex).unwrap(); + let base_tracklib_schema = ruby_schema.inner(); + let trimmed_tracklib_schema = trim_schema(base_tracklib_schema, &ruby_data); + let mut tracklib_section = + tracklib::write::section::encrypted::Section::new(rust_key_material, trimmed_tracklib_schema) + .map_err(|e| VM::raise(Class::from_existing("Exception"), &format!("{:?}", e))) + .unwrap(); + write_ruby_array_into_section(&mut tracklib_section, ruby_data); + + Module::from_existing("Tracklib").get_nested_class("Section").wrap_data( + WrappableSection { + section: tracklib::write::section::Section::Encrypted(tracklib_section), + }, + &*SECTION_WRAPPER_INSTANCE, + ) + }, +); + +fn trim_schema(schema: &tracklib::schema::Schema, data: &Array) -> tracklib::schema::Schema { + // Find all the fields used in all rows of the data + let mut keys = HashSet::new(); + for i in 0..data.length() { + let ruby_row_obj = data.at(i64::try_from(i) + .map_err(|_| VM::raise(Class::from_existing("Exception"), "array len larger than i64")) + .unwrap()); + let ruby_row = ruby_row_obj.try_convert_to::().map_err(VM::raise_ex).unwrap(); + ruby_row.each(|k, _| { + keys.insert(k.try_convert_to::().map_err(VM::raise_ex).unwrap().to_string()); + }); + } + + // subset original schema + let fields = schema + .fields() + .iter() + .filter_map(|field_def| { + if keys.contains(field_def.name()) { + Some(field_def.clone()) + } else { + None + } + }) + .collect::>(); + + if fields.len() != keys.len() { + VM::raise(Class::from_existing("Exception"), "Schema is missing field(s)"); + } + + tracklib::schema::Schema::with_fields(fields) +} + +fn write_ruby_array_into_section(section: &mut SW, data: Array) { + for ruby_row_obj in data { + let ruby_row = ruby_row_obj.try_convert_to::().map_err(VM::raise_ex).unwrap(); + + let mut rowbuilder = section.open_row_builder(); + + while let Some(column_writer) = rowbuilder.next_column_writer() { + match column_writer { + tracklib::write::section::writer::ColumnWriter::I64ColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + cwi.write(Some(&match ruby_field.try_convert_to::() { + Ok(i) => i.to_i64(), + Err(int_e) => ruby_field + .try_convert_to::() + .map_err(|_| VM::raise_ex(int_e)) + .unwrap() + .to_f64() + .round() as i64, + })) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + tracklib::write::section::writer::ColumnWriter::F64ColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + cwi.write(Some( + &Float::implicit_to_f(ruby_field).map_err(VM::raise_ex).unwrap().to_f64(), + )) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + tracklib::write::section::writer::ColumnWriter::U64ColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + cwi.write(Some(&match ruby_field.try_convert_to::() { + Ok(i) => i.to_u64(), + Err(int_e) => ruby_field + .try_convert_to::() + .map_err(|_| VM::raise_ex(int_e)) + .unwrap() + .to_f64() + .round() as u64, + })) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + tracklib::write::section::writer::ColumnWriter::BoolColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + cwi.write(Some( + &ruby_field + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap() + .to_bool(), + )) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + tracklib::write::section::writer::ColumnWriter::StringColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + cwi.write(Some( + ruby_field + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap() + .to_str(), + )) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + tracklib::write::section::writer::ColumnWriter::BoolArrayColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + let array = ruby_field.try_convert_to::().map_err(VM::raise_ex).unwrap(); + + let bool_vec = array + .into_iter() + .map(|ele| ele.try_convert_to::().map_err(VM::raise_ex).unwrap().to_bool()) + .collect::>(); + + cwi.write(Some(bool_vec.as_slice())) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + tracklib::write::section::writer::ColumnWriter::U64ArrayColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + let array = ruby_field.try_convert_to::().map_err(VM::raise_ex).unwrap(); + + let u64_vec = array + .into_iter() + .map(|ele| match ele.try_convert_to::() { + Ok(i) => i.to_u64(), + Err(int_e) => ele + .try_convert_to::() + .map_err(|_| VM::raise_ex(int_e)) + .unwrap() + .to_f64() + .round() as u64, + }) + .collect::>(); + + cwi.write(Some(u64_vec.as_slice())) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + tracklib::write::section::writer::ColumnWriter::ByteArrayColumnWriter(cwi) => { + let ruby_field_name = RString::from(String::from(cwi.field_definition().name())); + let ruby_field = ruby_row.at(&ruby_field_name); + + let write_result = if ruby_field.is_nil() { + cwi.write(None) + } else { + cwi.write(Some( + ruby_field + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap() + .to_bytes_unchecked(), + )) + }; + + if let Err(e) = write_result { + VM::raise(Class::from_existing("Exception"), &format!("{:?}", e)); + } + } + } + } + } +} + +impl VerifiedObject for Section { + fn is_correct_type(object: &T) -> bool { + object.class() == Module::from_existing("Tracklib").get_nested_class("Section") + } + + fn error_message() -> &'static str { + "Error converting to Section" + } +} + +class!(Time); +impl VerifiedObject for Time { + fn is_correct_type(object: &T) -> bool { + object.class() == Class::from_existing("Time") + } + + fn error_message() -> &'static str { + "Error converting to Time" + } +} + +module!(Tracklib); + +methods!( + Tracklib, + rtself, + fn write_track(metadata: Array, sections: Array) -> RString { + let metadata_array = metadata.map_err(VM::raise_ex).unwrap(); + + let metadata_entries = metadata_array + .into_iter() + .map(|metadata_ele| { + let metadata_ele_array = metadata_ele.try_convert_to::().map_err(VM::raise_ex).unwrap(); + if metadata_ele_array.length() >= 1 { + let metadata_type = metadata_ele_array + .at(0) + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap(); + match metadata_type.to_str() { + "track_type" => { + if metadata_ele_array.length() == 3 { + let track_type_symbol = metadata_ele_array + .at(1) + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap(); + let track_id = metadata_ele_array + .at(2) + .try_convert_to::() + .map_err(VM::raise_ex) + .unwrap() + .to_u64(); + + let track_type = match track_type_symbol.to_str() { + "route" => tracklib::types::TrackType::Route(track_id), + "trip" => tracklib::types::TrackType::Trip(track_id), + "segment" => tracklib::types::TrackType::Segment(track_id), + val => { + VM::raise( + Class::from_existing("Exception"), + &format!("Metadata Entry Track Type '{val}' unknown"), + ); + unreachable!(); + } + }; + + tracklib::types::MetadataEntry::TrackType(track_type) + } else { + VM::raise( + Class::from_existing("Exception"), + "Metadata Entries for 'track_type' must have length 3", + ); + unreachable!(); + } + } + "created_at" => { + if metadata_ele_array.length() == 2 { + let created_at_time_obj = metadata_ele_array + .at(1) + .try_convert_to::