The earlier C# (EasyHook) and C++ (MS Detours) ports were merged into
this repo under csharp/ and cpp/ so their full histories stay reachable
(git log -- csharp/ cpp/). Their trees are removed from HEAD because
their IPC tick epoch (delta against DateTime.UtcNow.Ticks) is
incompatible with the Rust implementation's raw FILETIME contract —
keeping both in the working tree would present two contradictory IPC
designs side by side.
The Java test target remains in the working tree; README links now
point at in-repo paths instead of the retired port repositories.
Close two LOCKED spec deviations from design-decisions.md flagged by
code-review of 18020e2:
- Q5: Enter key inside the popup now triggers Apply (apply_fake_time +
close), Esc cancels without committing. Both checks use consume_key
AFTER the inner widgets have rendered so a focused DragValue keeps
its own commit-on-Enter / cancel-on-Esc semantics.
- "Hit-area sizing" decision: day-grid buttons bumped from 28x22 px
(slightly cramped) to 32x32 px (spec minimum). Popup still fits in
the 260..=320 px min/max width band.
No public API change. apply_fake_time, picked_naive_dt, and the
DST/overflow status_msg flow are untouched.
Replace the six DragValue spinboxes (year/month/day/hour/minute/second)
with a single datetime button that opens one popup containing:
- month-navigable calendar grid (chevrons clamp at MIN_YEAR-01 / MAX_YEAR-12)
- 6x7 day grid via chrono::NaiveDate arithmetic; out-of-month cells dimmed
and jump-to-month on click; today gets a 1px outline; selected day filled
- HH:MM:SS DragValues for time (same control style as before)
- quick-select row: Now / Midnight / Noon / -1d / +1d
- right-aligned Apply primary action; Esc and click-outside close without
committing (CloseOnClickOutside + manual Esc capture)
Top bar reduces to [Mock Time] [datetime ▼] [Now] Δ = +X.Xs. The Set
button is gone — Apply (inside the popup) is the only commit path now.
picker_view_year/month is decoupled from fake_*, so chevroning months
does not move the selection; opening the popup re-syncs the view to the
current fake_year/month.
Drop the now-dead pub(crate) fn days_in_month and its 10 tests — the new
calendar grid uses chrono's Duration::days and cell_date.day() directly,
both of which are self-clamping. apply_fake_time, picked_naive_dt,
unix_micros_to_filetime_ticks, and the DST/overflow status_msg flow are
unchanged.
Creating a `Global\TimeMocker_<pid>` mapping requires
`SeCreateGlobalPrivilege`, which an unelevated token (e.g. debug `cargo run`
where the UAC manifest is intentionally skipped) does not have, so
`CreateFileMappingW` returned NULL with `ERROR_ACCESS_DENIED` and the
auto-inject scanner spammed `create MMF Global\TimeMocker_<pid>` for every
enumerated PID.
The UI now tries `Global\` first and falls back to `Local\` on access
denied, with a one-shot log warning so the auto-inject loop doesn't
flood. The hook DLL probes both names on attach so the IPC pairs up
symmetrically. Release builds keep their elevated `Global\` cross-session
reach; debug builds work against same-session targets without admin.
Also adds a dedicated `time-mocker-test-target` crate — a small console
binary that prints all 5 hooked time APIs every second with its own PID
banner, so dev verification can inject into a controlled harness instead
of arbitrary running processes.
Build and publish a GitHub Release on each pushed v* tag (also runs on
workflow_dispatch with a required tag input). The job runs on
windows-latest, tests + clippies + release-builds the workspace via
the pinned nightly toolchain, then zips time_mocker_ui.exe +
time_mocker_hook.dll + LICENSE + README into
time-mocker-<tag>-windows-x64.zip with a sha256 sidecar.
Workspace-wide refactor that fixes correctness, safety, and concurrency
issues across the three crates while preserving the public CLI/UX surface.
core:
- Split SharedDelta into SharedDeltaReader / SharedDeltaWriter so the
access-mode (FILE_MAP_READ vs ALL_ACCESS) is encoded in the type.
- Use AtomicI64::from_ptr on the page-aligned mapped view instead of a
raw *mut AtomicI64 cast.
- Detect ERROR_ALREADY_EXISTS via CreateOutcome and surface it.
- Move MMF name to the Global\ namespace so cross-session injection is
no longer silently scoped to the controller's session.
- Add tick-math round-trip tests (i64 + SYSTEMTIME boundary cases).
hook:
- install_hook! macro collapses the five hook installs to a table;
per-hook failures are collected into InstallReport instead of
aborting mid-chain and leaving a partial state armed.
- Unify fake_filetime helpers behind a single trampoline-parameterised fn.
- Fix GetLocalTime: previously returned UTC; now goes
FILETIME(UTC) -> SYSTEMTIME -> SystemTimeToTzSpecificLocalTime so DST
is resolved against the source date, matching real GetLocalTime.
- Replace thread::spawn from DllMain with raw CreateThread and
DisableThreadLibraryCalls(hinst) to avoid loader-lock deadlocks;
close the returned thread handle to plug a per-injection kernel leak.
- Propagate the real NtQuerySystemTime NTSTATUS instead of always
returning STATUS_SUCCESS.
- Return STATUS_ACCESS_VIOLATION on null out-pointer.
- Pipe InstallReport + MMF-open failures to OutputDebugStringW for
DbgView visibility in the target process.
ui:
- New win32_process_info module: pe_machine reads up to 64 KiB so PEs
with large e_lfanew values parse cleanly; query_full_image_name and
is_native_x64 (IsWow64Process2) gate inject against PID reuse and
WoW64 / non-AMD64 targets.
- Drop for InjectionManager zeroes every injected process's delta so
targets return to real time on UI exit.
- inject() reorders checks so the system-process guard runs against the
filename derived from the live image path, not the stale watcher
snapshot; failures are pushed to the ring-buffer log so auto-inject
loops are no longer silent.
- Refuse to inject critical Windows processes (csrss, smss, lsass,
services, svchost, MsMpEng, ...) explicitly.
- Switch the log from unbounded Vec to a VecDeque ring buffer (cap 1000).
- Rename eject -> disable to match what it actually does (zero delta,
keep DLL loaded).
- Unicode-aware paths_equivalent via to_lowercase comparison so non-ASCII
case differences don't yield false-positive PID-reuse errors.
- app.rs date/time picker: switch to DragValue so mid-typing keystrokes
don't rewrite the date; clamp year 1970-2200; checked arithmetic on
unix_micros -> FILETIME so far-future inputs don't silently overflow;
surface DST-gap status to the user; "Now" auto-applies.
- Add 51 unit tests across mmf, PE parser, system-process guards, time
math; expose helpers via pub(crate) for test access.
- Drop unused serde_json and thiserror deps.
Total: 56/56 tests passing, cargo clippy clean, cargo build --release
produces time_mocker_ui.exe and time_mocker_hook.dll.
Repo renamed from tiennm99/time-mocker-rs to tiennm99/time-mocker.
README and workspace Cargo.toml updated to reflect the new canonical
identity; C# and C++ ports linked as language siblings.
Repo renamed from tiennm99/time-mocker to tiennm99/time-mocker-csharp.
The canonical TimeMocker project is now the Rust implementation at
tiennm99/time-mocker.
Rust workspace with three crates:
- time-mocker-core: shared MockTimeInfo + named MMF helper + FILETIME helpers
- time-mocker-hook: cdylib injected into targets; inline detours via retour
on GetSystemTime / GetLocalTime / GetSystemTimeAsFileTime /
GetSystemTimePreciseAsFileTime / NtQuerySystemTime
- time-mocker-ui: egui controller with process list, time picker, and
glob/regex auto-inject rules; injects via dll-syringe; UAC manifest
embedded in release builds
IPC: 8-byte MMF named TimeMocker_<pid> holding an i64 delta in FILETIME
ticks. Hook adds delta to the real FILETIME on every call. Note this
differs from the C# version's .NET-tick delta -- contracts are not
interoperable.
Requires nightly toolchain (retour uses unboxed_closures / tuple_trait);
pinned via rust-toolchain.toml.