app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
Drew Owens
0.9.0: bridge data moved out of the install cache (recovery survives updates), closed-in-place records, gates route thr…
5d02ca9
20d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
//! HOW MUCH ROOM IS LEFT, and what this sweep is about to need.
//!
//! 0.7.2 verifies every recovery file by container marker and size, and pass 24
//! found the hole that leaves: the marker sits at offset 4, so ANY prefix of a
//! real document longer than eight bytes carries it. A write that reaches 20 KB
//! of a 500 KB assembly passes the marker AND the 16 KB floor. Marker and floor
//! together catch "empty" and "not a SOLIDWORKS file"; neither catches **the
//! right file, cut short**, and the format offers nothing cheap that would — no
//! constant tail, no length field, no head/tail relationship (measured).
//!
//! So the remaining defence is not in the file. It is in the decision to START:
//! this sweep writes every open document to disk, and it must refuse to begin
//! when the volume holds less room than the session it is about to write. A
//! truncation it cannot detect afterwards is one it declines to cause.
use std::path::Path;
/// Free bytes available on the volume holding `dir`, or `None` when that cannot
/// be measured.
///
/// `None` is an honest UNKNOWN and callers must treat it as one: this bridge does
/// not read "I could not measure the disk" as "there is room". The rule this
/// project keeps relearning is that an unanswerable question is not permission.
pub fn free_bytes(dir: &Path) -> Option<u64> {
// A TEST KNOB, and the only way to hold a nearly-full volume in a suite.
// A comma-separated list is consumed one entry per read, the last repeating,
// so a scenario can be "room to start, and then the disk fills" — which is
// the case that needs re-checking inside the loop rather than once at the
// top.
if let Ok(v) = std::env::var("SWX_TEST_FREE_BYTES") {
let vals: Vec<u64> = v.split(',').filter_map(|x| x.trim().parse().ok()).collect();
if !vals.is_empty() {
let f = crate::scratch::root().join("test-free-bytes-reads");
let n = std::fs::read_to_string(&f)
.ok()
.and_then(|t| t.trim().parse::<usize>().ok())
.unwrap_or(0);
let _ = std::fs::write(&f, (n + 1).to_string());
return Some(vals[n.min(vals.len() - 1)]);
}
}
platform_free_bytes(dir)
}
#[cfg(windows)]
fn platform_free_bytes(dir: &Path) -> Option<u64> {
use windows::core::HSTRING;
use windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
// The directory need not exist yet — the sweep creates it — so ask about the
// nearest ancestor that does.
let mut probe = dir.to_path_buf();
while !probe.exists() {
match probe.parent() {
Some(p) if p != probe => probe = p.to_path_buf(),
_ => break,
}
}
let mut free: u64 = 0;
let h = HSTRING::from(probe.as_os_str());
// FREE TO THE CALLER, not total free: on a volume with a quota those differ,
// and the one that matters is what this process may actually write.
unsafe { GetDiskFreeSpaceExW(&h, Some(&mut free), None, None) }.ok()?;
Some(free)
}
#[cfg(not(windows))]
fn platform_free_bytes(dir: &Path) -> Option<u64> {
// The bridge runs on Windows; this path exists so the suite measures a real
// volume rather than a stub. `df -Pk` is POSIX-specified output: one header
// line, then a row whose fourth field is available 1K-blocks.
let mut probe = dir.to_path_buf();
while !probe.exists() {
match probe.parent() {
Some(p) if p != probe => probe = p.to_path_buf(),
_ => break,
}
}
let out = std::process::Command::new("df").arg("-Pk").arg(&probe).output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
let row = text.lines().nth(1)?;
let avail_kb: u64 = row.split_whitespace().nth(3)?.parse().ok()?;
Some(avail_kb.saturating_mul(1024))
}
/// What one pathless document is assumed to cost when nothing better is known
/// and its kind is not known either.
pub const UNKNOWN_DOCUMENT_ESTIMATE: u64 = 1024 * 1024;
/// WHAT KIND OF DOCUMENT, because the three kinds are not the same size.
///
/// Measured over the 166 real documents in this container:
///
/// ```text
/// n min median max
/// .SLDPRT 99 29,130 134,885 37,200,505
/// .SLDASM 38 95,492 4,139,430 16,644,232
/// .SLDDRW 29 319,803 1,958,169 32,250,713
/// ```
///
/// An assembly is thirty times the median part. Costing one as if it were a part
/// is how a per-save gate passes with far less room than the write needs.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DocKind {
Part,
Assembly,
Drawing,
}
impl DocKind {
/// From a FILE NAME'S EXTENSION, and nothing else. `None` when the extension
/// is not one of the three.
///
/// 0.7.4 substring-matched the whole hint, so a recovery folder called
/// `…\Drawings\` reclassified every file under it as a drawing and a genuine
/// median-sized part (134,885 bytes) was refused against the 256 KB drawing
/// floor. A directory name is not evidence about a document.
///
/// It returns `Option` because **the classifier must be able to say "I do not
/// know"**. When it could not, every failure of the hint chain resolved to
/// `Part` — the LOWEST floor on both sides — so an unreadable type produced
/// the most permissive answer available. The two callers want opposite
/// defaults for an unknown (the estimate wants generous, the verification
/// wants strict), and neither can express that against a total function.
pub fn from_extension(name: &str) -> Option<DocKind> {
// The FILE's extension: a directory may contain dots, and everything
// before the last separator is somebody's folder name, not this
// document's kind.
let file = name.rsplit(['/', '\\']).next().unwrap_or(name);
let ext = file.rsplit_once('.').map(|(_, e)| e)?.to_ascii_lowercase();
match ext.as_str() {
"sldprt" => Some(DocKind::Part),
"sldasm" => Some(DocKind::Assembly),
"slddrw" => Some(DocKind::Drawing),
_ => None,
}
}
/// The general hint: an extension, and only an extension.
pub fn from_hint(hint: &str) -> Option<DocKind> {
Self::from_extension(hint)
}
/// A type NAME as the backends report it (`type` on the real one, `docType`
/// on the simulator). Exact, not substring: this is a closed vocabulary.
pub fn from_type_name(name: &str) -> Option<DocKind> {
match name.trim().to_ascii_lowercase().as_str() {
"part" => Some(DocKind::Part),
"assembly" => Some(DocKind::Assembly),
"drawing" => Some(DocKind::Drawing),
_ => None,
}
}
/// A TITLE: its extension if it has one, else the prefix SOLIDWORKS gives an
/// unsaved document — `Assem1`, `Part3`, `Draw2`.
///
/// The default title of a new assembly is `Assem1`, which 0.7.4 classified as
/// a PART and costed at 256 KB — where having no hint at all would have cost
/// the generic 1 MB, and the truth is an 8 MB assembly floor. A hint that
/// makes the answer worse than no hint is worse than useless, and this is the
/// common case rather than a corner.
pub fn from_title(title: &str) -> Option<DocKind> {
if let Some(k) = Self::from_extension(title) {
return Some(k);
}
let t = title.trim().to_ascii_lowercase();
if t.starts_with("assem") {
Some(DocKind::Assembly)
} else if t.starts_with("draw") {
Some(DocKind::Drawing)
} else if t.starts_with("part") {
Some(DocKind::Part)
} else {
None
}
}
/// The extension a recovery file of this kind is given.
pub fn extension(self) -> &'static str {
match self {
DocKind::Part => "SLDPRT",
DocKind::Assembly => "SLDASM",
DocKind::Drawing => "SLDDRW",
}
}
/// The name of this kind as the backends report it.
pub fn type_name(self) -> &'static str {
match self {
DocKind::Part => "part",
DocKind::Assembly => "assembly",
DocKind::Drawing => "drawing",
}
}
/// The least this kind of document is assumed to cost when its size cannot be
/// measured. Each sits above the measured MEDIAN for its kind, because an
/// estimate that is too low starts a write that cannot finish, and one that
/// is too high refuses a sweep that would have worked.
pub fn estimate_floor(self) -> u64 {
match self {
DocKind::Part => 256 * 1024,
DocKind::Assembly => 8 * 1024 * 1024,
DocKind::Drawing => 4 * 1024 * 1024,
}
}
/// THE FLOOR TO APPLY WHEN THE KIND IS NOT KNOWN: the highest of the three.
///
/// 0.7.4's comment argued the reverse — that a low default was right because
/// "too high a floor refuses to close a real document". Both halves are true
/// and the conclusion was backwards. A too-high floor REFUSES, loudly, naming
/// the file and the number; the user loses nothing and runs it again. A
/// too-low floor CLOSES a document against a file nobody checked. For a
/// verification, refusing is the direction to fail in.
pub fn unknown_kind_floor() -> u64 {
DocKind::Drawing.min_recovery_bytes()
}
/// The least a written recovery file of this kind may be and still count.
/// Each sits just BELOW the smallest real document of its kind measured here
/// (29,130 / 95,492 / 319,803), so it can never reject a genuine document
/// while still catching a write that stopped early.
///
/// **THE PART FLOOR IS TIGHT, and the first live close said so.** On
/// 2026-09-17 the sweep rescued four documents at 25,773-26,047 bytes each —
/// an EMPTY part is about 25.8 KB, which clears the 24 KB floor by roughly
/// 1.2 KB. That is the intended margin and not an accident: the floor exists
/// to catch a write that stopped early, and an empty part is the smallest
/// thing a real save can legitimately produce. But it is 5%, so anyone raising
/// this number is raising it INTO real documents, and must measure a genuinely
/// empty part on the target edition before touching it.
pub fn min_recovery_bytes(self) -> u64 {
match self {
DocKind::Part => 24 * 1024,
DocKind::Assembly => 80 * 1024,
DocKind::Drawing => 256 * 1024,
}
}
}
/// The safety factor between what this sweep will write and the room it insists
/// on having first.
///
/// SaveAs3 does not write in place: a document is built and flushed, the old copy
/// of a re-saved file can still be on the volume while the new one lands, and the
/// estimate itself is only an estimate. Two times the estimate is the margin, and
/// it is applied to every check — the one before the sweep starts and the one
/// before each save.
pub const HEADROOM_FACTOR: u64 = 2;
/// WHAT THIS SWEEP IS ABOUT TO WRITE.
///
/// A document with a readable path costs about what it costs on disk now. One
/// with no path, or whose path this process cannot stat, costs an ESTIMATE that
/// never falls below the floor for its KIND. Unknowns are rounded UP, because the
/// direction of a wrong estimate decides whether this refuses a sweep it could
/// have done (a nuisance) or starts one it cannot finish (a truncated file it
/// cannot detect).
pub struct WriteEstimate {
/// Total bytes this sweep is expected to write.
pub total: u64,
/// The largest single document, which is the room the NEXT save needs.
pub largest: u64,
/// How many documents had a size this process could actually read.
pub measured: usize,
/// How many were estimated instead.
pub estimated: usize,
}
/// Estimate from paths alone, inferring each document's kind from its extension.
/// A pathless document has no extension to read, so it costs the generic unknown.
pub fn estimate_writes(paths: &[String]) -> WriteEstimate {
let docs: Vec<(String, Option<DocKind>)> = paths
.iter()
.map(|p| {
let kind = if p.trim().is_empty() { None } else { DocKind::from_hint(p) };
(p.clone(), kind)
})
.collect();
estimate_writes_typed(&docs)
}
/// Estimate when the caller KNOWS each document's kind — which the sweep does,
/// because SOLIDWORKS told it.
///
/// The rule that matters is MONOTONICITY: **a document this process could measure
/// must never lower the assumed cost of one it could not.** 0.7.3 costed an
/// unknown at twice the largest measured document, so a single 29 KB part beside
/// a pathless assembly dropped that assembly's assumed cost from 1 MB to 58 KB —
/// learning something small about the session made the guess about the rest of it
/// smaller, and `largest` also gates the re-check before every save. The fix is
/// one `max` against a floor that depends only on the KIND.
pub fn estimate_writes_typed(docs: &[(String, Option<DocKind>)]) -> WriteEstimate {
let sizes: Vec<Option<u64>> = docs
.iter()
.map(|(p, _)| {
if p.trim().is_empty() {
None
} else {
std::fs::metadata(p).ok().map(|m| m.len())
}
})
.collect();
let biggest_known = sizes.iter().flatten().copied().max().unwrap_or(0);
let neighbour_cost = biggest_known.saturating_mul(2);
let mut total = 0u64;
let mut largest = 0u64;
let mut measured = 0usize;
let mut estimated = 0usize;
for (s, (_, kind)) in sizes.iter().zip(docs.iter()) {
let cost = match s {
Some(n) => {
measured += 1;
*n
}
None => {
estimated += 1;
// NEVER BELOW THE KIND'S FLOOR, whatever the neighbours weigh.
let floor = kind.map(|k| k.estimate_floor()).unwrap_or(UNKNOWN_DOCUMENT_ESTIMATE);
neighbour_cost.max(floor)
}
};
total = total.saturating_add(cost);
largest = largest.max(cost);
}
WriteEstimate { total, largest, measured, estimated }
}