Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 6 additions & 15 deletions rust_core/benches/core_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Performance benchmarks for DoubleTrack core operations
// Measures throughput of critical paths

use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use doubletrack_core::*;
use rand::SeedableRng;

Expand All @@ -27,17 +27,13 @@ fn benchmark_profile_serialization(c: &mut Criterion) {
let profile = gen.generate();

c.bench_function("profile_to_json", |b| {
b.iter(|| {
serde_json::to_string(black_box(&profile))
});
b.iter(|| serde_json::to_string(black_box(&profile)));
});

let json_str = serde_json::to_string(&profile).unwrap();

c.bench_function("json_to_profile", |b| {
b.iter(|| {
serde_json::from_str::<Profile>(black_box(&json_str))
});
b.iter(|| serde_json::from_str::<Profile>(black_box(&json_str)));
});
}

Expand Down Expand Up @@ -77,8 +73,7 @@ fn benchmark_activity_count_scaling(c: &mut Criterion) {
let mut total_duration = std::time::Duration::ZERO;
for _ in 0..iters {
let start = std::time::Instant::now();
let mut simulator =
ActivitySimulator::new(black_box(profile.clone()));
let mut simulator = ActivitySimulator::new(black_box(profile.clone()));
// Estimate hours needed for target activity count
let hours = ((activity_count as f64 / 4.0) as u32).max(1);
let _ = simulator.generate_activities(black_box(hours));
Expand Down Expand Up @@ -109,9 +104,7 @@ fn benchmark_schedule_generation(c: &mut Criterion) {
let profile = gen.generate();

c.bench_function("schedule_generation", |b| {
b.iter(|| {
Schedule::from_profile(black_box(&profile))
});
b.iter(|| Schedule::from_profile(black_box(&profile)));
});
}

Expand All @@ -120,9 +113,7 @@ fn benchmark_validation(c: &mut Criterion) {
let profile = gen.generate();

c.bench_function("profile_validation", |b| {
b.iter(|| {
profile.is_valid()
});
b.iter(|| profile.is_valid());
});
}

Expand Down
7 changes: 3 additions & 4 deletions rust_core/src/form_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,9 @@ impl FormDataGenerator {
// `first` is either "user" (literal fallback) or a yield from
// split_whitespace which never returns empty slices, so first_lower
// is always non-empty and .chars().next() is always Some.
first_lower
.chars()
.next()
.expect("first_lower is non-empty (literal fallback or split_whitespace yield)"),
first_lower.chars().next().expect(
"first_lower is non-empty (literal fallback or split_whitespace yield)"
),
last_lower,
number_suffix
),
Expand Down
55 changes: 43 additions & 12 deletions rust_core/src/interests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,16 @@ impl InterestUrlGenerator {
let title = self.get_video_title(interest, rng);
let video_id = self.generate_video_id(rng);

let platform = ["youtube.com", "vimeo.com"].choose(rng).expect(".choose() on a non-empty static literal array is always Some");
let platform = ["youtube.com", "vimeo.com"]
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some");
let url = if *platform == "youtube.com" {
format!("https://www.youtube.com/watch?v={}", video_id)
} else {
format!("https://vimeo.com/{}", rng.random_range(100000000..999999999))
format!(
"https://vimeo.com/{}",
rng.random_range(100000000..999999999)
)
};

(url, title)
Expand Down Expand Up @@ -107,7 +112,9 @@ impl InterestUrlGenerator {
("bsky.app", "Bluesky"),
];

let (domain, name) = platforms.choose(rng).expect(".choose() on a non-empty static literal array is always Some");
let (domain, name) = platforms
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some");
let url = format!("https://{}", domain);
let title = format!("Home - {}", name);

Expand Down Expand Up @@ -434,7 +441,10 @@ impl InterestUrlGenerator {
"recipe of the day",
],
};
queries.choose(rng).expect(".choose() on a non-empty static literal array is always Some").to_string()
queries
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
.to_string()
} else {
[
"news today",
Expand Down Expand Up @@ -517,7 +527,10 @@ impl InterestUrlGenerator {
"How-To Guide Step by Step",
],
};
titles.choose(rng).expect(".choose() on a non-empty static literal array is always Some").to_string()
titles
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
.to_string()
} else {
"Trending Video".to_string()
}
Expand Down Expand Up @@ -601,7 +614,10 @@ impl InterestUrlGenerator {
"New Arrival",
],
};
products.choose(rng).expect(".choose() on a non-empty static literal array is always Some").to_string()
products
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
.to_string()
} else {
"Product".to_string()
}
Expand Down Expand Up @@ -658,7 +674,10 @@ impl InterestUrlGenerator {
"Weather Pattern Shift Expected This Week",
],
};
headlines.choose(rng).expect(".choose() on a non-empty static literal array is always Some").to_string()
headlines
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
.to_string()
} else {
"Breaking News".to_string()
}
Expand Down Expand Up @@ -728,7 +747,10 @@ impl InterestUrlGenerator {
"Literature Review Summary",
],
};
topics.choose(rng).expect(".choose() on a non-empty static literal array is always Some").to_string()
topics
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
.to_string()
} else {
"General Topic".to_string()
}
Expand Down Expand Up @@ -766,7 +788,10 @@ impl InterestUrlGenerator {
"Community Recommendations",
],
};
pages.choose(rng).expect(".choose() on a non-empty static literal array is always Some").to_string()
pages
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
.to_string()
} else {
"General Page".to_string()
}
Expand Down Expand Up @@ -848,15 +873,21 @@ impl DomainDatabase {
}

fn get_shopping_domain<R: Rng>(&self, rng: &mut R) -> &str {
self.shopping.choose(rng).expect(".choose() on a non-empty static literal array is always Some")
self.shopping
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
}

fn get_news_domain<R: Rng>(&self, rng: &mut R) -> &str {
self.news.choose(rng).expect(".choose() on a non-empty static literal array is always Some")
self.news
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
}

fn get_research_domain<R: Rng>(&self, rng: &mut R) -> &str {
self.research.choose(rng).expect(".choose() on a non-empty static literal array is always Some")
self.research
.choose(rng)
.expect(".choose() on a non-empty static literal array is always Some")
}

fn get_interest_domain<R: Rng>(
Expand Down
27 changes: 17 additions & 10 deletions rust_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,23 @@ pub fn generate_profile(seed: Option<u64>) -> JsValue {
let profile = generator.generate();
// Profile is a plain serde-derived struct of owned primitives/Vecs; serialization
// to a JsValue cannot fail. WASM-export signature returns JsValue, not Result.
serde_wasm_bindgen::to_value(&profile).expect("Profile serialization is infallible (plain owned data)")
serde_wasm_bindgen::to_value(&profile)
.expect("Profile serialization is infallible (plain owned data)")
}

/// Generate browsing activities for a profile
#[wasm_bindgen]
pub fn generate_activities(profile_json: JsValue, duration_hours: u32) -> JsValue {
// Caller-supplied JsValue must deserialize as Profile; validated by JS shim.
// WASM-export signature returns JsValue, not Result — callers should pre-validate.
let profile: Profile = serde_wasm_bindgen::from_value(profile_json)
.expect("profile_json must be a valid serialized Profile (validate via validate_profile first)");
let profile: Profile = serde_wasm_bindgen::from_value(profile_json).expect(
"profile_json must be a valid serialized Profile (validate via validate_profile first)",
);
let mut simulator = ActivitySimulator::new(profile);
let activities = simulator.generate_activities(duration_hours);
// Vec<BrowsingActivity> is plain serde-derived owned data; serialization is infallible.
serde_wasm_bindgen::to_value(&activities).expect("BrowsingActivity serialization is infallible (plain owned data)")
serde_wasm_bindgen::to_value(&activities)
.expect("BrowsingActivity serialization is infallible (plain owned data)")
}

/// Validate that a profile is internally consistent
Expand All @@ -58,23 +61,27 @@ pub fn validate_profile(profile_json: JsValue) -> bool {
#[wasm_bindgen]
pub fn get_activity_schedule(profile_json: JsValue) -> JsValue {
// Caller-supplied JsValue must deserialize as Profile; validated by JS shim.
let profile: Profile = serde_wasm_bindgen::from_value(profile_json)
.expect("profile_json must be a valid serialized Profile (validate via validate_profile first)");
let profile: Profile = serde_wasm_bindgen::from_value(profile_json).expect(
"profile_json must be a valid serialized Profile (validate via validate_profile first)",
);
let schedule = Schedule::from_profile(&profile);
// Schedule is plain serde-derived owned data; serialization is infallible.
serde_wasm_bindgen::to_value(&schedule).expect("Schedule serialization is infallible (plain owned data)")
serde_wasm_bindgen::to_value(&schedule)
.expect("Schedule serialization is infallible (plain owned data)")
}

/// Generate plausible form fill data tied to a profile
#[wasm_bindgen]
pub fn generate_form_data(profile_json: JsValue) -> JsValue {
// Caller-supplied JsValue must deserialize as Profile; validated by JS shim.
let profile: Profile = serde_wasm_bindgen::from_value(profile_json)
.expect("profile_json must be a valid serialized Profile (validate via validate_profile first)");
let profile: Profile = serde_wasm_bindgen::from_value(profile_json).expect(
"profile_json must be a valid serialized Profile (validate via validate_profile first)",
);
let mut rng = rand::rngs::SmallRng::from_os_rng();
let form = FormDataGenerator::generate(&profile, &mut rng);
// FormData is plain serde-derived owned data; serialization is infallible.
serde_wasm_bindgen::to_value(&form).expect("FormData serialization is infallible (plain owned data)")
serde_wasm_bindgen::to_value(&form)
.expect("FormData serialization is infallible (plain owned data)")
}

#[cfg(test)]
Expand Down
24 changes: 12 additions & 12 deletions rust_core/tests/aspect_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,7 @@ fn test_sql_injection_patterns_in_formdata() {
let form = FormDataGenerator::generate(&profile, &mut rng);

// Email should always be valid format, not SQL injection
assert!(
form.email.contains('@'),
"Email must contain @ symbol"
);
assert!(form.email.contains('@'), "Email must contain @ symbol");
let parts: Vec<&str> = form.email.split('@').collect();
assert_eq!(parts.len(), 2, "Email must have valid structure");

Expand Down Expand Up @@ -151,7 +148,7 @@ fn test_activity_timestamp_validity() {
// Activity timestamps should be recent (within 24 hours)
let time_diff = (now - activity.timestamp).abs();
assert!(
time_diff < 86400 * 2, // 2 days tolerance
time_diff < 86400 * 2, // 2 days tolerance
"Activity timestamp should be recent"
);
}
Expand All @@ -178,10 +175,7 @@ fn test_activity_list_consistency() {
// All activities should have consistent structure
for activity in &activities {
// Duration should be positive
assert!(
activity.duration_seconds > 0,
"Duration must be positive"
);
assert!(activity.duration_seconds > 0, "Duration must be positive");

// URL should be valid
assert!(
Expand Down Expand Up @@ -239,8 +233,14 @@ fn test_no_unsafe_pattern_in_data_structures() {
let form = FormDataGenerator::generate(&profile, &mut rng);

// Check for null bytes (would cause C string issues)
assert!(!form.email.contains('\0'), "Email must not contain null bytes");
assert!(!form.display_name.contains('\0'), "Display name must not contain null bytes");
assert!(
!form.email.contains('\0'),
"Email must not contain null bytes"
);
assert!(
!form.display_name.contains('\0'),
"Display name must not contain null bytes"
);

// Check for control characters (except newline/tab which might be ok)
for c in form.email.chars() {
Expand All @@ -264,7 +264,7 @@ fn test_boundary_values_for_activity_duration() {
let max_duration = match activity.activity_type {
ActivityType::VideoWatch => 3600 * 4, // Videos might be longer
ActivityType::Research => 3600 * 2, // Research could be extended
_ => 3600, // Most activities < 1 hour
_ => 3600, // Most activities < 1 hour
};

assert!(
Expand Down
6 changes: 2 additions & 4 deletions rust_core/tests/e2e_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ fn test_full_profile_lifecycle() {
assert!(!json_str.is_empty(), "Serialized JSON must not be empty");

// Deserialize back
let deserialized: Profile =
serde_json::from_str(&json_str).expect("Deserialization failed");
let deserialized: Profile = serde_json::from_str(&json_str).expect("Deserialization failed");

// Verify data integrity
assert_eq!(
Expand Down Expand Up @@ -182,8 +181,7 @@ fn test_formdata_serialization_roundtrip() {
let form_original = FormDataGenerator::generate(&profile, &mut rng);

// Serialize
let json_str =
serde_json::to_string(&form_original).expect("FormData serialization failed");
let json_str = serde_json::to_string(&form_original).expect("FormData serialization failed");

// Deserialize
let form_deserialized: FormData =
Expand Down
3 changes: 1 addition & 2 deletions rust_core/tests/property_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ use rand::SeedableRng;
// Define strategies for property-based testing

fn name_strategy() -> impl Strategy<Value = String> {
r"[A-Z][a-z]{1,10} [A-Z][a-z]{1,10}"
.prop_map(|s| s.to_string())
r"[A-Z][a-z]{1,10} [A-Z][a-z]{1,10}".prop_map(|s| s.to_string())
}

fn age_strategy() -> impl Strategy<Value = u8> {
Expand Down
2 changes: 1 addition & 1 deletion src/bindings/Chrome.res.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading