├── .github └── workflows │ └── test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── data ├── .gitignore └── README.md └── src ├── cli.rs ├── create.rs ├── db ├── mod.rs ├── prefixes.rs └── types.rs ├── lib.rs ├── main.rs ├── runner.rs ├── telemetry.rs ├── tree.rs └── utils.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | profile: minimal 13 | toolchain: nightly 14 | override: true 15 | - uses: Swatinem/rust-cache@v2 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: nightly 28 | override: true 29 | - uses: Swatinem/rust-cache@v2 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | args: --all 34 | build: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: nightly 42 | override: true 43 | - uses: Swatinem/rust-cache@v2 44 | - uses: actions-rs/cargo@v1 45 | with: 46 | command: build 47 | args: --all 48 | 49 | fmt: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | - uses: actions-rs/toolchain@v1 54 | with: 55 | profile: minimal 56 | toolchain: nightly 57 | override: true 58 | components: rustfmt 59 | - uses: Swatinem/rust-cache@v2 60 | - uses: actions-rs/cargo@v1 61 | with: 62 | command: fmt 63 | args: --all -- --check 64 | 65 | clippy: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v3 69 | - uses: actions-rs/toolchain@v1 70 | with: 71 | profile: minimal 72 | toolchain: nightly 73 | override: true 74 | components: clippy 75 | - uses: Swatinem/rust-cache@v2 76 | - uses: actions-rs/cargo@v1 77 | with: 78 | command: clippy 79 | args: --all -- -D warnings 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .DS_Store 13 | 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gprobe" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["refcell "] 6 | readme = "README.md" 7 | repository = "https://github.com/refcell/gprobe/" 8 | license = "MIT" 9 | description = """ 10 | A verbose CLI to probe go-ethereum data structures. 11 | """ 12 | keywords = ["rust", "evm", "data", "database", "ethereum"] 13 | 14 | [dependencies] 15 | clap = { version = "3.1.18", features = ["derive"] } 16 | uuid = { version = "1.1.1", features = ["v4"] } 17 | 18 | # Telemtry 19 | spinners = "4.1.0" 20 | isatty = "0.1.9" 21 | yansi = "0.5.1" 22 | ansi_term = "0.12.1" 23 | tracing = "0.1.36" 24 | tracing-bunyan-formatter = "0.3.4" 25 | tracing-log = "0.1.3" 26 | tracing-subscriber = { version = "0.3.16", features = ["fmt", "env-filter", "ansi"] } 27 | chrono = "0.4.22" 28 | teloxide = { version = "0.12", features = ["macros"] } 29 | rusty-leveldb = "1.0.6" 30 | eyre = "0.6.8" 31 | flate2 = "1.0.25" 32 | tar = "0.4.38" 33 | serde_json = "1.0.93" 34 | serde = { version = "1.0.152", features = ["derive"] } 35 | rlp = "0.5.2" 36 | hex = "0.4.3" 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 refcell.eth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## gprobe 2 | 3 | [![build](https://github.com/refcell/gprobe/actions/workflows/test.yml/badge.svg)](https://github.com/refcell/gprobe/actions/workflows/test.yml) 4 | [![license: MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) 5 | [![Crates.io][crates-badge]][crates-url] 6 | 7 | [crates-badge]: https://img.shields.io/crates/v/gprobe.svg 8 | [crates-url]: https://crates.io/crates/gprobe 9 | 10 | A verbose CLI to probe go-ethereum data structures, built in rust. 11 | 12 | ### Quickstart 13 | 14 | _Prerequisites: Install Rust: https://www.rust-lang.org/tools/install_ 15 | 16 | Install as a global command: 17 | 18 | ```bash 19 | cargo install --git github.com/refcell/gprobe --branch main 20 | ``` 21 | 22 | Then, you can run `gprobe` from anywhere. Alternatively, you can run `cargo run` from the project root or build the project with `cargo build`. 23 | 24 | To use gprobe as a library, add the following to your `Cargo.toml`: 25 | 26 | ```toml 27 | gprobe = { git = "https://github.com/refcell/gprobe", branch = "main" } 28 | ``` 29 | 30 | ### Reference 31 | 32 | ```bash 33 | grpobe 0.1.2 34 | A verbose CLI to probe go-ethereum data structures. 35 | 36 | USAGE: 37 | gprobe [OPTIONS] [SOURCE] [SUBCOMMAND] 38 | 39 | ARGS: 40 | The data source to probe 41 | 42 | OPTIONS: 43 | -h, --help Print help information 44 | -p, --print Prints out to the terminal 45 | -v, --verbose Verbose output 46 | -V, --version Print version information 47 | 48 | SUBCOMMANDS: 49 | create Create a new database 50 | decompress Decompress a Tarball 51 | help Print this message or the help of the given subcommand(s) 52 | tree Traverse a database 53 | ``` 54 | 55 | ### Documentation 56 | 57 | `gprobe` exposes a number of useful utilities. The `tree` subcommand traverses a database and prints out the hex-encoded keys and values. 58 | 59 | 60 | ### Contributing 61 | 62 | All contributions are welcome. Before opening a PR, please submit an issue detailing the bug or feature. When opening a PR, please ensure that your changes build with nightly rust, has been linted with `cargo fmt`, and contains tests when applicable. 63 | 64 | ### License 65 | 66 | [MIT](https://choosealicense.com/licenses/mit/) 67 | 68 | ### Disclaimer 69 | 70 | _This code is being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the code. It has not been audited and as such there can be no assurance it will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. Nothing in this repo should be construed as investment advice or legal advice for any particular facts or circumstances and is not meant to replace competent counsel. It is strongly advised for you to contact a reputable attorney in your jurisdiction for any questions or concerns with respect thereto. The creators are not liable for any use of the foregoing, and users should proceed with caution and use at their own risk._ 71 | 72 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | *.tar 2 | *.tar.gz 3 | *.tar.bz2 4 | *.tar.xz 5 | *.zip 6 | *.7z 7 | *.rar 8 | *.iso 9 | *.img 10 | *.bin 11 | *.dmg 12 | *.vdi 13 | *.vmdk 14 | *.vhd 15 | *.swp 16 | *.swo 17 | *.swn 18 | *.swm 19 | *.swx 20 | *.swpx 21 | 22 | geth 23 | 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | ## Data Directory 2 | 3 | This directory is intended to store large data stores for gprobing. 4 | 5 | ### Ignored Stores 6 | 7 | The following stores in this directory are ignored by git: 8 | 9 | - `*.tar` 10 | - `*.tar.gz` 11 | - `*.tar.bz2` 12 | - `*.tar.xz` 13 | - `*.zip` 14 | - `*.7z` 15 | - `*.rar` 16 | - `*.iso` 17 | - `*.img` 18 | - `*.bin` 19 | - `*.dmg` 20 | - `*.vdi` 21 | - `*.vmdk` 22 | - `*.vhd` 23 | - `*.swp` 24 | - `*.swo` 25 | - `*.swn` 26 | - `*.swm` 27 | - `*.swx` 28 | - `*.swpx` -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | /// GProbe Cli Args 4 | #[derive(Parser, Debug, Clone)] 5 | #[clap(name = "grpobe", version, about, long_about = None)] 6 | pub struct GProbe { 7 | /// The data source to probe. 8 | pub source: Option, 9 | 10 | /// Prints out to the terminal. 11 | #[clap(short = 'p', long = "print")] 12 | pub print: bool, 13 | 14 | /// Verbose output. 15 | #[clap(short = 'v', long = "verbose")] 16 | pub verbose: bool, 17 | 18 | /// GProbe Subcommands 19 | #[clap(subcommand)] 20 | pub subcommand: Option, 21 | } 22 | 23 | /// GProbe Subcommands 24 | #[derive(Subcommand, Clone, Debug)] 25 | pub enum Subcommands { 26 | /// Traverse a database 27 | Tree { 28 | /// The depth of the tree 29 | #[clap(short = 'l', long = "level", default_value = "3")] 30 | level: u64, 31 | 32 | /// The path of the database 33 | #[clap(short = 'p', long = "path")] 34 | path: Option, 35 | }, 36 | /// Create a new database 37 | Create { 38 | /// The path of the database 39 | #[clap(short = 'p', long = "path")] 40 | path: Option, 41 | }, 42 | /// Decompress a Tarball 43 | Decompress { 44 | /// The path of the tarball 45 | #[clap(short = 'p', long = "path")] 46 | path: Option, 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /src/create.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use rusty_leveldb::{LdbIterator, Options, DB}; 4 | use spinners::Spinner; 5 | 6 | /// Creates a new database at the provided path 7 | pub fn create(path: impl AsRef, spinner: &mut Option) { 8 | tracing::debug!("Creating db \"{}\"", path.as_ref().to_string_lossy()); 9 | 10 | // Open the leveldb-rs database at the given path 11 | let open_options = Options { 12 | create_if_missing: true, 13 | ..Default::default() 14 | }; 15 | let mut db = DB::open(path.as_ref(), open_options).unwrap(); 16 | 17 | // Insert value and flush to disk 18 | db.put(b"key", b"value").unwrap(); 19 | db.flush().unwrap(); 20 | 21 | // Create a new db iterator 22 | let mut iter = match db.new_iter() { 23 | Ok(iter) => iter, 24 | Err(e) => { 25 | tracing::error!("Failed to create leveldb iterator: {}", e); 26 | return; 27 | } 28 | }; 29 | 30 | // Stop the spinner so we can print 31 | if let Some(sp) = spinner { 32 | sp.stop(); 33 | println!(); 34 | } 35 | *spinner = None; 36 | 37 | // Warn if db is empty 38 | if iter.next().is_none() { 39 | tracing::warn!("Error: Database is empty"); 40 | } 41 | iter.reset(); 42 | 43 | // Walk entire db 44 | while let Some((k, v)) = iter.next() { 45 | let key = String::from_utf8_lossy(k.as_slice()); 46 | let value = String::from_utf8_lossy(v.as_slice()); 47 | println!("Key: {}, Value: {}", key, value); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | //! LevelDB Go-Ethereum Database Types 2 | 3 | /// Table Prefixes 4 | mod prefixes; 5 | pub use prefixes::*; 6 | 7 | /// [go-ethereum](https://github.com/ethereum/go-ethereum) database types 8 | mod types; 9 | pub use types::*; 10 | -------------------------------------------------------------------------------- /src/db/prefixes.rs: -------------------------------------------------------------------------------- 1 | //! [go-ethereum](https://github.com/ethereum/go-ethereum) Table Prefixes 2 | 3 | /// Header prefix + num (uint64 big endian) + hash -> header 4 | pub const HEADER_PREFIX: &[u8] = b"h"; 5 | 6 | /// Header Prefix + num (uint64 big endian) + hash + Header TD Suffix -> td 7 | pub const HEADER_TD_SUFFIX: &[u8] = b"t"; 8 | 9 | /// Header Prefix + num (uint64 big endian) + hash + Header Hash Suffix -> hash 10 | pub const HEADER_NUMBER_PREFIX: &[u8] = b"H"; 11 | 12 | /// Block Body Prefix + num (uint64 big endian) + hash -> block body 13 | pub const BLOCK_BODY_PREFIX: &[u8] = b"b"; 14 | 15 | /// Block Receipts Prefix + num (uint64 big endian) + hash -> block receipts 16 | pub const BLOCK_RECEIPTS_PREFIX: &[u8] = b"r"; 17 | 18 | /// Transaction Lookup Prefix + hash -> transaction/receipt lookup metadata 19 | pub const TX_LOOKUP_PREFIX: &[u8] = b"l"; 20 | 21 | /// Bloom Bits Prefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits 22 | pub const BLOOM_BITS_PREFIX: &[u8] = b"B"; 23 | 24 | /// Snapshot Account Prefix + account hash -> account trie value 25 | pub const SNAPSHOT_ACCOUNT_PREFIX: &[u8] = b"a"; 26 | 27 | /// Snapshot Storage Prefix + account hash + storage hash -> storage trie value 28 | pub const SNAPSHOT_STORAGE_PREFIX: &[u8] = b"o"; 29 | 30 | /// Code Prefix + code hash -> account code 31 | pub const CODE_PREFIX: &[u8] = b"c"; 32 | 33 | /// Skeleton Header Prefix + num (uint64 big endian) -> header 34 | pub const SKELETON_HEADER_PREFIX: &[u8] = b"S"; 35 | -------------------------------------------------------------------------------- /src/db/types.rs: -------------------------------------------------------------------------------- 1 | // pub trait FromLevelBytes { 2 | // fn from_level_bytes(bytes: &[u8]) -> ; 3 | // } 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(dead_code)] 3 | #![allow(clippy::enum_variant_names)] 4 | #![warn(missing_docs)] 5 | #![warn(unused_extern_crates)] 6 | #![forbid(unsafe_code)] 7 | #![forbid(where_clauses_object_safety)] 8 | #![allow(deprecated)] 9 | 10 | //! # GProbe 11 | //! 12 | //! GProbe is a tool for probing the data source and generating a schema for it. 13 | //! 14 | //! ## Usage 15 | //! 16 | //! // TODO: 17 | 18 | /// Cli Args 19 | pub mod cli; 20 | 21 | /// Module wrapping database utilities 22 | pub mod db; 23 | 24 | /// Telemetry 25 | pub mod telemetry; 26 | 27 | /// The core [crate::cli::GProbe] runner 28 | pub mod runner; 29 | 30 | /// The tree dispatch 31 | pub mod tree; 32 | 33 | /// The create dispatch 34 | pub mod create; 35 | 36 | /// Utilities 37 | pub mod utils; 38 | 39 | /// A group of handlers for the [crate::runner] module to dispatch 40 | pub mod handlers { 41 | pub use super::create::*; 42 | /// The [crate::handlers::tree] handler 43 | pub use super::tree::*; 44 | } 45 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use gprobe::cli; 4 | 5 | fn main() { 6 | cli::GProbe::parse().run(); 7 | } 8 | -------------------------------------------------------------------------------- /src/runner.rs: -------------------------------------------------------------------------------- 1 | use clap::CommandFactory; 2 | use flate2::read::GzDecoder; 3 | use isatty::stdout_isatty; 4 | use spinners::{Spinner, Spinners}; 5 | use std::{fs::File, thread}; 6 | use tar::Archive; 7 | 8 | use crate::{ 9 | cli::{GProbe, Subcommands}, 10 | telemetry, 11 | }; 12 | 13 | impl GProbe { 14 | /// Runs the core logic for the [gprobe::cli::GProbe] CLI Application 15 | pub fn run(&mut self) { 16 | let mut app = GProbe::into_app(); 17 | 18 | // Set up tracing 19 | let subscriber = match self.verbose { 20 | true => { 21 | println!("Initializing verbose subscriber..."); 22 | telemetry::get_subscriber("debug".into()) 23 | } 24 | false => telemetry::get_subscriber("info".into()), 25 | }; 26 | telemetry::init_subscriber(subscriber); 27 | 28 | // Match on subcommand 29 | match &self.subcommand { 30 | Some(Subcommands::Tree { level, path }) => { 31 | let p = match self.source { 32 | Some(_) => self.source.clone(), 33 | None => path.clone(), 34 | }; 35 | self.tree(*level, p); 36 | } 37 | Some(Subcommands::Create { path }) => { 38 | let p = match self.source { 39 | Some(_) => self.source.clone(), 40 | None => path.clone(), 41 | }; 42 | self.create(p); 43 | } 44 | Some(Subcommands::Decompress { path }) => { 45 | let p = match self.source { 46 | Some(_) => self.source.clone(), 47 | None => path.clone(), 48 | }; 49 | match p { 50 | Some(p) => self.decompress(&p), 51 | None => { 52 | println!("No path provided for tarball"); 53 | } 54 | } 55 | } 56 | None => { 57 | if app.print_help().is_err() { 58 | tracing::warn!("Failed to print help menu for gprobe command"); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// Decompress a tarball 65 | pub fn decompress(&self, p: &str) { 66 | match File::open(p) { 67 | Ok(tar_gz) => { 68 | let tar = GzDecoder::new(tar_gz); 69 | let mut archive = Archive::new(tar); 70 | if let Err(e) = archive.unpack(".") { 71 | tracing::error!("Failed to decompress tarball: {}", e); 72 | } 73 | } 74 | Err(e) => { 75 | tracing::error!("Failed to open tarball: {}", e); 76 | } 77 | } 78 | } 79 | 80 | /// Prints out a tree of the data source 81 | pub fn tree(&mut self, level: u64, path: Option) { 82 | GProbe::spin("Probing...", |sp| { 83 | tracing::debug!("Tracing a datastore tree with a depth of {}", level); 84 | thread::sleep(std::time::Duration::from_secs(2)); 85 | 86 | let path = match path { 87 | Some(path) => path, 88 | None => { 89 | tracing::warn!("No path provided for datastore tree"); 90 | return; 91 | } 92 | }; 93 | crate::handlers::walk(path, level, sp); 94 | 95 | tracing::debug!("Done tracing datastore tree"); 96 | }); 97 | } 98 | 99 | /// Creates a new database at the provided path 100 | pub fn create(&mut self, path: Option) { 101 | GProbe::spin("Creating...", |sp| { 102 | thread::sleep(std::time::Duration::from_secs(2)); 103 | 104 | let path = match path { 105 | Some(path) => path, 106 | None => { 107 | tracing::warn!("No path provided for datastore tree"); 108 | return; 109 | } 110 | }; 111 | crate::handlers::create(path, sp); 112 | 113 | tracing::debug!("Done tracing datastore tree"); 114 | }); 115 | } 116 | 117 | /// Wraps the callback in a spinner if stdout is a TTY 118 | fn spin(prompt: impl std::fmt::Display, callback: impl FnOnce(&mut Option)) { 119 | // If stdout is a TTY, create a spinner 120 | let mut sp: Option = None; 121 | if stdout_isatty() { 122 | sp = Some(Spinner::new(Spinners::Dots, format!("{}", prompt))); 123 | } 124 | 125 | callback(&mut sp); 126 | 127 | // Stop spinner animation if it exists 128 | if let Some(mut sp) = sp { 129 | sp.stop(); 130 | println!(" "); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/telemetry.rs: -------------------------------------------------------------------------------- 1 | use tracing::subscriber::set_global_default; 2 | use tracing::{Level, Subscriber}; 3 | use tracing_log::LogTracer; 4 | use tracing_subscriber::Layer; 5 | use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; 6 | 7 | use ansi_term::Colour::{Blue, Cyan, Purple, Red, Yellow}; 8 | 9 | /// The AnsiVisitor 10 | pub struct AnsiVisitor; 11 | 12 | impl tracing::field::Visit for AnsiVisitor { 13 | fn record_f64(&mut self, _: &tracing::field::Field, value: f64) { 14 | println!("{value}") 15 | } 16 | 17 | fn record_i64(&mut self, _: &tracing::field::Field, value: i64) { 18 | println!("{value}") 19 | } 20 | 21 | fn record_u64(&mut self, _: &tracing::field::Field, value: u64) { 22 | println!("{value}") 23 | } 24 | 25 | fn record_bool(&mut self, _: &tracing::field::Field, value: bool) { 26 | println!("{value}") 27 | } 28 | 29 | fn record_str(&mut self, _: &tracing::field::Field, value: &str) { 30 | println!("{value}") 31 | } 32 | 33 | fn record_error( 34 | &mut self, 35 | _: &tracing::field::Field, 36 | value: &(dyn std::error::Error + 'static), 37 | ) { 38 | println!("{value}") 39 | } 40 | 41 | fn record_debug(&mut self, _: &tracing::field::Field, value: &dyn std::fmt::Debug) { 42 | println!("{value:?}") 43 | } 44 | } 45 | 46 | /// An Ansi Term layer for tracing 47 | pub struct AsniTermLayer; 48 | 49 | impl Layer for AsniTermLayer 50 | where 51 | S: tracing::Subscriber, 52 | { 53 | fn on_event( 54 | &self, 55 | event: &tracing::Event<'_>, 56 | _ctx: tracing_subscriber::layer::Context<'_, S>, 57 | ) { 58 | // Print the timestamp 59 | let utc: chrono::DateTime = chrono::Utc::now(); 60 | print!("[{}] ", Cyan.paint(utc.to_rfc2822())); 61 | 62 | // Print the level prefix 63 | match *event.metadata().level() { 64 | Level::ERROR => { 65 | eprint!("{}: ", Red.paint("ERROR")); 66 | } 67 | Level::WARN => { 68 | print!("{}: ", Yellow.paint("WARN")); 69 | } 70 | Level::INFO => { 71 | print!("{}: ", Blue.paint("INFO")); 72 | } 73 | Level::DEBUG => { 74 | print!("DEBUG: "); 75 | } 76 | Level::TRACE => { 77 | print!("{}: ", Purple.paint("TRACE")); 78 | } 79 | } 80 | 81 | print!("{} ", Purple.paint(event.metadata().target())); 82 | print!( 83 | "at {} ", 84 | Cyan.paint( 85 | event 86 | .metadata() 87 | .name() 88 | .split(' ') 89 | .last() 90 | .unwrap_or_default() 91 | ) 92 | ); 93 | let mut visitor = AnsiVisitor; 94 | event.record(&mut visitor); 95 | } 96 | } 97 | 98 | /// Subscriber Composer 99 | /// 100 | /// Builds a subscriber with multiple layers into a [tracing](https://crates.io/crates/tracing) subscriber. 101 | pub fn get_subscriber(env_filter: String) -> impl Subscriber + Sync + Send { 102 | let env_filter = 103 | EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); 104 | let formatting_layer = AsniTermLayer; 105 | Registry::default().with(env_filter).with(formatting_layer) 106 | } 107 | 108 | /// Globally registers a subscriber. 109 | /// 110 | /// ### Panics 111 | /// 112 | /// If it is called more than once. 113 | pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) { 114 | LogTracer::init().expect("Failed to set logger"); 115 | set_global_default(subscriber).expect("Failed to set subscriber"); 116 | } 117 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use rusty_leveldb::{LdbIterator, Options, DB}; 4 | use spinners::Spinner; 5 | 6 | use crate::utils; 7 | 8 | /// Walks the tree in a depth-first manner, calling the given function on each node. 9 | pub fn walk(path: impl AsRef, level: u64, spinner: &mut Option) { 10 | let db_name = path.as_ref().to_string_lossy(); 11 | tracing::debug!("Walking db \"{}\" tree to a depth of {}", db_name, level); 12 | 13 | // Cleanup the database directory 14 | if let Err(e) = utils::cleanup_db_dir(&db_name) { 15 | tracing::error!("Failed to cleanup database directory: {}", e); 16 | return; 17 | } 18 | 19 | // Open the leveldb-rs database at the given path 20 | let open_options = Options { 21 | create_if_missing: false, 22 | ..Default::default() 23 | }; 24 | let mut db = DB::open(path.as_ref(), open_options).unwrap(); 25 | 26 | // Create a new db iterator 27 | let mut iter = match db.new_iter() { 28 | Ok(iter) => iter, 29 | Err(e) => { 30 | tracing::error!("Failed to create leveldb iterator: {}", e); 31 | return; 32 | } 33 | }; 34 | 35 | // Stop the spinner so we can print 36 | if let Some(sp) = spinner { 37 | sp.stop(); 38 | println!(); 39 | } 40 | *spinner = None; 41 | 42 | // Warn if db is empty 43 | if iter.next().is_none() { 44 | tracing::warn!("Error: Database is empty"); 45 | } 46 | iter.reset(); 47 | 48 | // Walk the database 49 | while let Some((k, v)) = iter.next() { 50 | let key = format!("0x{}", hex::encode(&k)); 51 | let value = format!("0x{}", hex::encode(&v)); 52 | println!("Key: {}\nValue: {}\n", key, value); 53 | } 54 | println!("Finished walking db \"{}\"", db_name); 55 | } 56 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | use std::{fs, path::Path}; 3 | 4 | /// Clean up the database directory 5 | pub fn cleanup_db_dir(base: &str) -> Result<()> { 6 | delete_obsolete_files(base)?; 7 | let prefix = base.strip_suffix('/').unwrap_or(base); 8 | let dir = format!("{}/{}", prefix, "ancient"); 9 | println!("Hoisting up a level: {}", dir); 10 | hoist_dir_up(&dir)?; 11 | Ok(()) 12 | } 13 | 14 | /// Deletes obsolete files in a database directory. 15 | pub fn delete_obsolete_files(base: &str) -> Result<()> { 16 | let prefix_path = base.strip_suffix('/').unwrap_or(base); 17 | let obsolete_files = vec![".DS_Store", "CURRENT.bak"]; 18 | for file in obsolete_files { 19 | let file_path = format!("{}/{}", prefix_path, file); 20 | let path = Path::new(&file_path); 21 | if path.exists() { 22 | tracing::debug!("Deleting obsolete file {:?}", file_path); 23 | fs::remove_file(path)?; 24 | } 25 | } 26 | Ok(()) 27 | } 28 | 29 | /// Move a directory up a level in the filesystem 30 | pub fn hoist_dir_up(dir: &str) -> Result<()> { 31 | let path = Path::new(dir); 32 | if path.exists() { 33 | tracing::debug!("Found directory {:?}", path); 34 | let parent = path.parent().unwrap().parent().unwrap(); 35 | tracing::debug!("Hoisting up a level to \"{}\"...", parent.display()); 36 | let new_path = parent.join(path.file_name().unwrap()); 37 | tracing::debug!("Moving {:?} to {:?}", path, new_path); 38 | fs::rename(path, new_path)?; 39 | } 40 | Ok(()) 41 | } 42 | --------------------------------------------------------------------------------