app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
Drew Owens
Share frozen native Rust mechanical development checkpoint and qualification scope
b2d7cdb
27d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
//! The single read-settling allowance is available only after acknowledged effects.
use super::*;
pub(super) const MAX_CAPTURES: usize = 3;
pub(super) const MAX_SETTLING_DIAGNOSTIC_BYTES: usize = 8 * 1024;
#[derive(Clone, Debug)]
pub struct Capture {
pub projection: Projection,
pub root_after: Root,
}
pub(super) struct Trace {
captures: Vec<Capture>,
attempted: usize,
settling_used: bool,
projection_mismatch: Option<Value>,
component_revision_changes: Vec<usize>,
revision_payload_omitted: bool,
}
impl Trace {
pub(super) fn new() -> Result<Self> {
let mut captures = Vec::new();
captures
.try_reserve_exact(MAX_CAPTURES)
.map_err(|_| "native_assembly_readback_capacity")?;
let mut component_revision_changes = Vec::new();
component_revision_changes
.try_reserve_exact(MAX_COMPONENTS)
.map_err(|_| "native_assembly_readback_capacity")?;
Ok(Self {
captures,
attempted: 0,
settling_used: false,
projection_mismatch: None,
component_revision_changes,
revision_payload_omitted: false,
})
}
pub(super) fn next<B: Backend>(&mut self, b: &B, h: &B::Handle, cloud: bool) -> Result<usize> {
if self.attempted >= MAX_CAPTURES {
return Err("native_assembly_readback_bound".into());
}
if b.command()? != "SelectCommand" {
return Err("native_assembly_command_changed_after_effect".into());
}
self.attempted += 1;
let captured = b.capture(h, cloud)?;
// The complete returned projection is retained before any validation/refusal.
self.captures.push(captured);
let i = self.captures.len() - 1;
let c = &self.captures[i];
if c.projection.cloud_metadata != cloud {
return Err("native_assembly_metadata_mode_mismatch".into());
}
validate(&c.projection)?;
if c.root_after.revision.is_empty()
|| !text(&c.root_after.revision)
|| !text(&c.root_after.name)
{
return Err("native_assembly_projection_bound".into());
}
Ok(i)
}
pub(super) fn evidence(&self) -> Value {
let captures:Vec<_>=self.captures.iter().enumerate().map(|(i,c)|{
let projection=bounded_json(&c.projection,MAX_BYTES);
let before=bounded_json(&c.projection.root,64*1024);
let after=bounded_json(&c.root_after,64*1024);
let digest=|v:&Option<Vec<u8>>|v.as_ref().map(|b|fusion_mechanical_journal::request_digest(b));
let roots_bounded=[&c.projection.root,&c.root_after].iter().all(|r|text(&r.name)&&text(&r.revision));
let changed=if roots_bounded {root_capture_diagnostic(&c.projection.root,&c.root_after)}else{
Some(json!({"kind":"invalid_root_text_omitted","payloadUnavailable":true,"expectedNameBytes":c.projection.root.name.len(),"observedNameBytes":c.root_after.name.len(),"expectedRevisionBytes":c.projection.root.revision.len(),"observedRevisionBytes":c.root_after.revision.len(),"expectedNameSha256":fusion_mechanical_journal::request_digest(c.projection.root.name.as_bytes()),"observedNameSha256":fusion_mechanical_journal::request_digest(c.root_after.name.as_bytes()),"expectedRevisionSha256":fusion_mechanical_journal::request_digest(c.projection.root.revision.as_bytes()),"observedRevisionSha256":fusion_mechanical_journal::request_digest(c.root_after.revision.as_bytes())}))
};
json!({"index":i,"projectionSha256":digest(&projection),"projectionBytes":projection.as_ref().map(Vec::len),"projectionJsonOmitted":projection.is_none(),"projectionContainsNonFinite":c.projection.occurrences.iter().any(|o|o.matrix.iter().any(|v|!v.is_finite())),"rootBeforeSha256":digest(&before),"rootAfterSha256":digest(&after),"rootStable":c.projection.root==c.root_after,"rootChangeDiagnostic":changed})
}).collect();
json!({"policy":"post-effect-initial-root-and-component-revision-settling-v2","maximumCaptures":MAX_CAPTURES,"captureAttempts":self.attempted,"settlingUsed":self.settling_used,"captures":captures,"firstProjectionDifference":self.projection_mismatch,"initialComponentRevisionChanges":self.component_changes(!self.revision_payload_omitted),"combinedSettlingDiagnosticLimitBytes":MAX_SETTLING_DIAGNOSTIC_BYTES,"componentRevisionPayloadUnavailable":self.revision_payload_omitted,"wholeProjectionsRetainedForComparison":true,"effectRetryAttempted":false,"acceptedRevisionIgnored":false})
}
fn component_changes(&self, expose: bool) -> Vec<Value> {
self.component_revision_changes.iter().map(|&i| {
let before=&self.captures[0].projection.components[i].revision;
let after=&self.captures[1].projection.components[i].revision;
json!({"componentSlot":i,"expected":if expose{Some(before)}else{None},"observed":if expose{Some(after)}else{None},"expectedBytes":before.len(),"observedBytes":after.len(),"expectedSha256":fusion_mechanical_journal::request_digest(before.as_bytes()),"observedSha256":fusion_mechanical_journal::request_digest(after.as_bytes()),"payloadUnavailable":!expose})
}).collect()
}
}
fn strict(c: &Capture) -> Result<()> {
if c.projection.root != c.root_after {
return Err("native_assembly_root_changed_during_capture".into());
}
Ok(())
}
pub(super) fn snapshot<B: Backend>(
b: &B,
h: &B::Handle,
cloud: bool,
t: &mut Trace,
) -> Result<Projection> {
let first = t.next(b, h, cloud)?;
let c = &t.captures[first];
if c.projection.root == c.root_after {
let second = t.next(b, h, cloud)?;
strict(&t.captures[second])?;
if t.captures[first].projection != t.captures[second].projection {
t.projection_mismatch = projection_difference(
&t.captures[first].projection,
&t.captures[second].projection,
"stable_pair",
);
return Err("native_assembly_projection_changed".into());
}
return Ok(t.captures[second].projection.clone());
}
let mut normalized = c.projection.clone();
normalized.root.revision = c.root_after.revision.clone();
if normalized.root != c.root_after {
return Err("native_assembly_root_changed_during_capture".into());
}
let diagnostic =
root_capture_diagnostic(&c.projection.root, &c.root_after).expect("changed root");
if serde_json::to_vec(&diagnostic)
.map_err(|_| "native_assembly_serialization")?
.len()
> MAX_SETTLING_DIAGNOSTIC_BYTES
{
return Err("native_assembly_settling_diagnostic_bound".into());
}
// One discarded initial capture only. No errors or later changes are retried.
t.settling_used = true;
let second = t.next(b, h, cloud)?;
strict(&t.captures[second])?;
// A first root drift permits only initial component revision witnesses to
// settle as well. Exact membership and every other field still compare.
if normalized.components.len() == t.captures[second].projection.components.len() {
for (i, (before, after)) in normalized
.components
.iter_mut()
.zip(&t.captures[second].projection.components)
.enumerate()
{
if before.revision != after.revision {
t.component_revision_changes.push(i);
before.revision = after.revision.clone();
}
}
}
// A failure while formatting/checking the combined budget must itself
// leave the safe hash-only representation selected in the retained trace.
t.revision_payload_omitted = true;
let combined = json!({"rootChangeDiagnostic":diagnostic,"componentRevisionChanges":t.component_changes(true)});
if bounded_json(&combined, MAX_SETTLING_DIAGNOSTIC_BYTES).is_none() {
// Preserve all changed slots and complete hashes/lengths without
// flooding the bounded observation with failed raw revision strings.
return Err("native_assembly_settling_diagnostic_bound".into());
}
t.revision_payload_omitted = false;
if normalized != t.captures[second].projection {
t.projection_mismatch = projection_difference(
&normalized,
&t.captures[second].projection,
"initial_projection_with_root_after_vs_first_stable",
);
return Err("native_assembly_settling_projection_changed".into());
}
let third = t.next(b, h, cloud)?;
strict(&t.captures[third])?;
if t.captures[second].projection != t.captures[third].projection {
t.projection_mismatch = projection_difference(
&t.captures[second].projection,
&t.captures[third].projection,
"settled_pair",
);
return Err("native_assembly_projection_changed".into());
}
Ok(t.captures[third].projection.clone())
}
// Fixed-cap serializer for diagnostics, including invalid backend observations.
// Rejected oversized input never causes an unbounded duplicate allocation.
fn bounded_json(value: &impl Serialize, limit: usize) -> Option<Vec<u8>> {
struct Writer {
bytes: Vec<u8>,
limit: usize,
}
impl std::io::Write for Writer {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
if bytes.len() > self.limit.saturating_sub(self.bytes.len()) {
return Err(std::io::Error::other("diagnostic bound"));
}
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut w = Writer {
bytes: Vec::new(),
limit,
};
w.bytes.try_reserve_exact(limit).ok()?;
serde_json::to_writer(&mut w, value).ok()?;
Some(w.bytes)
}
// A single bounded first difference avoids another diagnostic app cycle.
// Complete projections remain in Trace; evidence binds values without duplication.
pub(super) fn projection_difference(
before: &Projection,
after: &Projection,
comparison: &str,
) -> Option<Value> {
let expected = serde_json::to_value(before).ok()?;
let observed = serde_json::to_value(after).ok()?;
fn first<'a>(
a: Option<&'a Value>,
b: Option<&'a Value>,
path: String,
) -> Option<(String, Option<&'a Value>, Option<&'a Value>)> {
if a == b {
return None;
}
match (a, b) {
(Some(Value::Object(a)), Some(Value::Object(b))) => {
for (key, av) in a {
let escaped = key.replace('~', "~0").replace('/', "~1");
if let Some(d) = first(Some(av), b.get(key), format!("{path}/{escaped}")) {
return Some(d);
}
}
for (key, bv) in b {
if !a.contains_key(key) {
let escaped = key.replace('~', "~0").replace('/', "~1");
return Some((format!("{path}/{escaped}"), None, Some(bv)));
}
}
None
}
(Some(Value::Array(a)), Some(Value::Array(b))) => {
for i in 0..a.len().max(b.len()) {
if let Some(d) = first(a.get(i), b.get(i), format!("{path}/{i}")) {
return Some(d);
}
}
None
}
_ => Some((path, a, b)),
}
}
fn summary(v: Option<&Value>) -> Value {
let Some(v) = v else {
return json!({"present":false});
};
let bytes = serde_json::to_vec(v).expect("JSON value serializable");
let expose = !v.is_array() && !v.is_object() && bytes.len() <= 256;
json!({"present":true,"serializedBytes":bytes.len(),"sha256":fusion_mechanical_journal::request_digest(&bytes),"value":if expose{Some(v)}else{None},"payloadUnavailable":!expose})
}
let (path, a, b) = first(Some(&expected), Some(&observed), String::new())?;
let path_json = serde_json::to_vec(&path).ok()?;
let short = path_json.len() <= 256;
Some(
json!({"comparison":comparison,"path":if short{Some(&path)}else{None},"pathSha256":fusion_mechanical_journal::request_digest(path.as_bytes()),"pathBytes":path.len(),"pathUnavailable":!short,"expected":summary(a),"observed":summary(b),"additionalSdkReads":0}),
)
}