Skip to content

Commit 7d4db02

Browse files
committed
Merge branch 'feat/hash' into devel
2 parents e238a1f + 9d8fb8a commit 7d4db02

File tree

6 files changed

+176
-30
lines changed

6 files changed

+176
-30
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ deepsize = "0.2.0"
3434
tracing = "0.1.40"
3535
tracing-subscriber = { version = "0.3.18", optional = true }
3636
chrono = { version = "0.4.38", optional = false }
37+
blake3 = "1.5.4"
38+
serde_repr = "0.1.19"
3739

3840
[[bin]] # client
3941
name = "netpulse"

src/analyze.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,11 +375,10 @@ fn store_meta(store: &Store, f: &mut String) -> Result<(), AnalysisError> {
375375
let store_size_mem = store.deep_size_of();
376376
let store_size_fs = std::fs::metadata(Store::path())?.size();
377377

378-
key_value_write(f, "Hash Datastructure", store.display_hash())?;
379-
key_value_write(f, "Hash Store File", store.display_hash_of_file()?)?;
378+
key_value_write(f, "Hash mem blake3", store.get_hash())?;
379+
key_value_write(f, "Hash file sha256", store.get_hash_of_file()?)?;
380380
key_value_write(f, "Store Version (mem)", store.version())?;
381-
// TODO: find a way to get the version just from file without deserializing it
382-
key_value_write(f, "Store Version (file)", "<TODO>")?;
381+
key_value_write(f, "Store Version (file)", Store::peek_file_version()?)?;
383382
key_value_write(f, "Store Size (mem)", store_size_mem)?;
384383
key_value_write(f, "Store Size (file)", store_size_fs)?;
385384
key_value_write(

src/bins/netpulse.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use netpulse::common::{init_logging, print_usage};
1717
use netpulse::errors::RunError;
1818
use netpulse::records::{display_group, Check};
1919
use netpulse::store::Store;
20+
use tracing::error;
2021

2122
fn main() {
2223
init_logging(tracing::Level::INFO);
@@ -28,6 +29,11 @@ fn main() {
2829
opts.optflag("V", "version", "print the version");
2930
opts.optflag("t", "test", "test run all checks");
3031
opts.optflag("d", "dump", "print out all checks");
32+
opts.optflag(
33+
"r",
34+
"rewrite",
35+
"load store and immediately save to rewrite the file",
36+
);
3137
opts.optflag("f", "failed", "only consider failed checks for dumping");
3238
let matches = match opts.parse(&args[1..]) {
3339
Ok(m) => m,
@@ -50,7 +56,12 @@ fn main() {
5056
dump(failed_only);
5157
} else if matches.opt_present("test") {
5258
if let Err(e) = test_checks() {
53-
eprintln!("{e}");
59+
error!("{e}");
60+
std::process::exit(1)
61+
}
62+
} else if matches.opt_present("rewrite") {
63+
if let Err(e) = rewrite() {
64+
error!("{e}");
5465
std::process::exit(1)
5566
}
5667
} else {
@@ -93,6 +104,12 @@ fn dump(failed_only: bool) {
93104
println!("{buf}")
94105
}
95106

107+
fn rewrite() -> Result<(), RunError> {
108+
let s = Store::load()?;
109+
s.save()?;
110+
Ok(())
111+
}
112+
96113
fn analysis() {
97114
let store = store_load();
98115
match analyze::analyze(&store) {

src/errors.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ pub enum StoreError {
5353
/// Failed to load store data from file.
5454
///
5555
/// This typically indicates corruption or an incompatible / outdated store format.
56-
#[error("Could not load the store from file: {source}")]
56+
#[error("Could not deserialize the store from the loaded data: {source}")]
5757
Load {
5858
/// Underlying error
5959
#[from]
@@ -92,6 +92,9 @@ pub enum StoreError {
9292
/// is not defined. Only known [Versions][crate::store::Version] are valid.
9393
#[error("Tried to load a store version that does not exist: {0}")]
9494
BadStoreVersion(u8),
95+
/// A store can be loaded as readonly if it's corrupted or there is a version mismatch
96+
#[error("Tried to save a readonly store")]
97+
IsReadonly,
9598
}
9699

97100
/// Errors that can occur during network checks.

src/records.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
//! ```
4040
4141
use std::fmt::{Display, Write};
42-
use std::hash::{Hash, Hasher};
42+
use std::hash::Hash;
4343
use std::net::IpAddr;
4444

4545
use chrono::{DateTime, Local, TimeZone, Utc};
@@ -249,14 +249,27 @@ impl DeepSizeOf for Check {
249249
}
250250

251251
impl Check {
252-
/// Generates a hash of the in-memory [Check] data.
253-
///
254-
/// Uses [DefaultHasher](std::hash::DefaultHasher) to create a 16-character hexadecimal hash
255-
/// of the [Check] that can be used to identify this [Check]. Useful for detecting changes.
256-
pub fn get_hash(&self) -> String {
257-
let mut hasher = std::hash::DefaultHasher::default();
258-
self.hash(&mut hasher);
259-
format!("{:016X}", hasher.finish())
252+
/// Generates a cryptographic hash of the [Check] data.
253+
///
254+
/// Uses [blake3] for consistent hashing across Rust versions and platforms.
255+
/// The hash remains stable as long as the check's contents don't change,
256+
/// making it suitable for persistent identification of checks.
257+
///
258+
/// # Implementation Details
259+
///
260+
/// - Uses [bincode] for serialization of check data
261+
/// - Uses [blake3] for cryptographic hashing
262+
/// - Produces a 32-byte (256-bit) hash
263+
///
264+
/// # Panics
265+
///
266+
/// May panic if serialization fails, which can happen in extreme cases:
267+
/// - System is out of memory
268+
/// - System is in a severely degraded state
269+
///
270+
/// Normal [Check] data will always serialize successfully.
271+
pub fn get_hash(&self) -> blake3::Hash {
272+
blake3::hash(&bincode::serialize(&self).expect("serialization of a check failed"))
260273
}
261274

262275
/// Creates a new check result with the specified properties.

src/store.rs

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
1818
use std::fmt::Display;
1919
use std::fs::{self};
20-
use std::hash::{Hash, Hasher};
20+
use std::hash::Hash;
2121
use std::io::{ErrorKind, Write};
2222
use std::os::unix::fs::OpenOptionsExt;
2323
use std::path::PathBuf;
@@ -60,6 +60,14 @@ pub const ZSTD_COMPRESSION_LEVEL: i32 = 4;
6060
/// Primarily intended for development and testing.
6161
pub const ENV_PATH: &str = "NETPULSE_STORE_PATH";
6262

63+
/// How long to wait between running workloads for the daemon
64+
pub const DEFAULT_PERIOD: i64 = 60;
65+
/// Environment variable name for the time period after which the daemon wakes up.
66+
///
67+
/// If set, its value will be used instead of [DEFAULT_PERIOD].
68+
/// Primarily intended for development and testing.
69+
pub const ENV_PERIOD: &str = "NETPULSE_PERIOD";
70+
6371
/// Version information for the store format.
6472
///
6573
/// The [Store] definition might change over time as netpulse is developed. To work with older or
@@ -71,9 +79,20 @@ pub const ENV_PATH: &str = "NETPULSE_STORE_PATH";
7179
///
7280
/// This only describes the version of the [Store], not of [Netpulse](crate) itself.
7381
#[derive(
74-
Debug, PartialEq, Eq, Hash, Deserialize, Serialize, Copy, Clone, DeepSizeOf, PartialOrd, Ord,
82+
Debug,
83+
PartialEq,
84+
Eq,
85+
Hash,
86+
Copy,
87+
Clone,
88+
DeepSizeOf,
89+
PartialOrd,
90+
Ord,
91+
serde_repr::Serialize_repr,
92+
serde_repr::Deserialize_repr,
7593
)]
7694
#[allow(missing_docs)] // It's just versions man
95+
#[repr(u8)]
7796
pub enum Version {
7897
V0 = 0,
7998
V1 = 1,
@@ -91,6 +110,9 @@ pub struct Store {
91110
version: Version,
92111
/// Collection of all recorded checks
93112
checks: Vec<Check>,
113+
// if true, this store will never be saved
114+
#[serde(skip)]
115+
readonly: bool,
94116
}
95117

96118
impl Display for Version {
@@ -193,6 +215,7 @@ impl Store {
193215
Self {
194216
version: Version::CURRENT,
195217
checks: Vec::new(),
218+
readonly: false,
196219
}
197220
}
198221

@@ -395,13 +418,17 @@ impl Store {
395418

396419
let mut store: Store = bincode::deserialize_from(reader)?;
397420

398-
// TODO: somehow account for old versions that are not compatible with the store struct
399421
if store.version != Version::CURRENT {
400422
warn!("The store that was loaded is not of the current version: store has {} but the current version is {}", store.version, Version::CURRENT);
401423
if Version::SUPPROTED.contains(&store.version) {
402-
warn!("The old store version is still supported, migrating to newer version");
424+
warn!("The different store version is still supported, migrating to newer version");
403425
warn!("Temp migration in memory, can be made permanent by saving");
404426

427+
if store.version > Version::CURRENT {
428+
warn!("The store version is newer than this version of netpulse can normally handle! Trying to ignore potential differences and loading as READONLY!");
429+
store.readonly = true;
430+
}
431+
405432
while store.version < Version::CURRENT {
406433
for check in store.checks_mut().iter_mut() {
407434
if let Err(e) = check.migrate(Version::V0) {
@@ -437,8 +464,12 @@ impl Store {
437464
/// - File doesn't exist
438465
/// - Write fails
439466
/// - Serialization fails
467+
/// - Trying to save a readonly [Store]
440468
pub fn save(&self) -> Result<(), StoreError> {
441469
info!("Saving the store");
470+
if self.readonly {
471+
return Err(StoreError::IsReadonly);
472+
}
442473
let file = match fs::File::options()
443474
.read(false)
444475
.write(true)
@@ -478,19 +509,45 @@ impl Store {
478509
/// Returns the check interval in seconds.
479510
///
480511
/// This determines how frequently the daemon performs checks.
481-
/// Currently fixed at 60 seconds.
482-
pub const fn period_seconds(&self) -> i64 {
483-
60
512+
/// Default is [DEFAULT_PERIOD], but this value can be overridden by setting [ENV_PERIOD] as
513+
/// environment variable.
514+
pub fn period_seconds(&self) -> i64 {
515+
if let Ok(v) = std::env::var(ENV_PERIOD) {
516+
v.parse().unwrap_or(DEFAULT_PERIOD)
517+
} else {
518+
DEFAULT_PERIOD
519+
}
484520
}
485521

486-
/// Generates a hash of the in-memory store data.
522+
/// Generates a cryptographic hash of the entire [Store].
523+
///
524+
/// Uses [blake3] for consistent hashing across Rust versions and platforms.
525+
/// The hash changes when any check (or other field) in the store is modified,
526+
/// added, or removed.
527+
///
528+
/// # Implementation Details
487529
///
488-
/// Uses [DefaultHasher](std::hash::DefaultHasher) to create a 16-character hexadecimal hash
489-
/// of the entire store contents. Useful for detecting changes.
490-
pub fn display_hash(&self) -> String {
491-
let mut hasher = std::hash::DefaultHasher::default();
492-
self.hash(&mut hasher);
493-
format!("{:016X}", hasher.finish())
530+
/// - Uses [bincode] for serialization of store data
531+
/// - Uses [blake3] for cryptographic hashing
532+
/// - Produces a 32-byte (256-bit) hash
533+
/// - Performance scales linearly with store size
534+
///
535+
/// # Memory Usage
536+
///
537+
/// For a netpulsed running continuously:
538+
/// - ~34 bytes per check
539+
/// - ~50MB per year at 1 check/minute
540+
/// - Serialization and hashing remain efficient
541+
///
542+
/// # Panics
543+
///
544+
/// May panic if serialization fails, which can happen in extreme cases:
545+
/// - System is out of memory
546+
/// - System is in a severely degraded state
547+
///
548+
/// Normal [Store] data (checks, version info) will always serialize successfully.
549+
pub fn get_hash(&self) -> blake3::Hash {
550+
blake3::hash(&bincode::serialize(&self).expect("serialization of the store failed"))
494551
}
495552

496553
/// Generates SHA-256 hash of the store file on disk.
@@ -506,7 +563,7 @@ impl Store {
506563
/// Returns [StoreError] if:
507564
/// - sha256sum command fails
508565
/// - Output parsing fails
509-
pub fn display_hash_of_file(&self) -> Result<String, StoreError> {
566+
pub fn get_hash_of_file(&self) -> Result<String, StoreError> {
510567
let out = Command::new("sha256sum").arg(Self::path()).output()?;
511568

512569
if !out.status.success() {
@@ -577,6 +634,61 @@ impl Store {
577634
pub fn checks_mut(&mut self) -> &mut Vec<Check> {
578635
&mut self.checks
579636
}
637+
638+
/// Reads only the [Version] from a store file without loading the entire [Store].
639+
///
640+
/// This function efficiently checks the store version by:
641+
/// 1. Opening the store file (decompressing it if enabled)
642+
/// 2. Deserializing only the version field
643+
/// 3. Skipping the rest of the data
644+
///
645+
/// This is more efficient than loading the full store when only version
646+
/// information is needed, such as during version compatibility checks. It may also keep
647+
/// working if the format/version of the store is incompatible with what this version of
648+
/// netpulse uses.
649+
///
650+
/// # Feature Flags
651+
///
652+
/// If the "compression" feature is enabled, this function will decompress
653+
/// the store file using [zstd] before reading the version.
654+
///
655+
/// # Errors
656+
///
657+
/// Returns [StoreError] if:
658+
/// - Store file doesn't exist ([`StoreError::DoesNotExist`])
659+
/// - Store file is corrupt or truncated ([`StoreError::Load`])
660+
/// - File permissions prevent reading ([`StoreError::Io`])
661+
/// - Decompression fails (with "compression" feature) ([`StoreError::Io`])
662+
///
663+
/// # Examples
664+
///
665+
/// ```rust,no_run
666+
/// use netpulse::store::Store;
667+
/// use netpulse::errors::StoreError;
668+
///
669+
/// match Store::peek_file_version() {
670+
/// Ok(version) => println!("Store version in file: {}", version),
671+
/// Err(StoreError::DoesNotExist) => println!("No store file found"),
672+
/// Err(e) => eprintln!("Error reading store version: {}", e),
673+
/// }
674+
/// ```
675+
pub fn peek_file_version() -> Result<Version, StoreError> {
676+
#[derive(Deserialize)]
677+
struct VersionOnly {
678+
version: Version,
679+
#[serde(skip)]
680+
_rest: serde::de::IgnoredAny,
681+
}
682+
683+
let file = std::fs::File::open(Self::path())?;
684+
#[cfg(feature = "compression")]
685+
let reader = zstd::Decoder::new(file)?;
686+
#[cfg(not(feature = "compression"))]
687+
let reader = file;
688+
689+
let version_only: VersionOnly = bincode::deserialize_from(reader)?;
690+
Ok(version_only.version)
691+
}
580692
}
581693

582694
fn has_cap_net_raw() -> bool {

0 commit comments

Comments
 (0)