1#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
8
9use std::fmt;
10
11use crate::platform::{
12 decode_workspace_graph_json_v1, decode_workspace_snapshot_json_v1, WorkspaceGraph,
13 WorkspaceSnapshot,
14};
15use crate::tooling_products::{
16 decode_tooling_artifact_graph_v1, decode_tooling_build_trace_v1,
17 decode_tooling_compile_cost_report_v1, ToolingArtifactGraphV1, ToolingBuildTraceV1,
18 ToolingCompileCostReportV1, ARTIFACT_GRAPH_TOOLING_SCHEMA_V1, BUILD_TRACE_TOOLING_SCHEMA_V1,
19 COMPILE_COST_TOOLING_SCHEMA_V1,
20};
21use crate::tooling_schema::{
22 decode_tooling_schema_negotiation_request_v1, negotiate_tooling_schema_v1,
23};
24
25pub const WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1: &str = "presolve.workspace-snapshot";
26pub const WORKSPACE_GRAPH_TOOLING_SCHEMA_V1: &str = "presolve.workspace-graph";
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ToolingProductV1 {
30 WorkspaceSnapshot(WorkspaceSnapshot),
31 WorkspaceGraph(WorkspaceGraph),
32 BuildTrace(ToolingBuildTraceV1),
33 CompileCostReport(ToolingCompileCostReportV1),
34 ArtifactGraph(ToolingArtifactGraphV1),
35}
36
37impl ToolingProductV1 {
38 #[must_use]
39 pub const fn schema(&self) -> &'static str {
40 match self {
41 Self::WorkspaceSnapshot(_) => WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1,
42 Self::WorkspaceGraph(_) => WORKSPACE_GRAPH_TOOLING_SCHEMA_V1,
43 Self::BuildTrace(_) => BUILD_TRACE_TOOLING_SCHEMA_V1,
44 Self::CompileCostReport(_) => COMPILE_COST_TOOLING_SCHEMA_V1,
45 Self::ArtifactGraph(_) => ARTIFACT_GRAPH_TOOLING_SCHEMA_V1,
46 }
47 }
48
49 #[must_use]
50 pub const fn version(&self) -> u32 {
51 1
52 }
53
54 #[must_use]
55 pub fn snapshot_id(&self) -> &str {
56 match self {
57 Self::WorkspaceSnapshot(snapshot) => snapshot.snapshot_id.as_str(),
58 Self::WorkspaceGraph(graph) => graph.snapshot_id.as_str(),
59 Self::BuildTrace(trace) => trace.snapshot_id.as_deref().unwrap_or(""),
60 Self::CompileCostReport(report) => report.build_id.as_str(),
61 Self::ArtifactGraph(graph) => graph.build_id.as_str(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ToolingProductReadErrorV1 {
68 pub code: &'static str,
69 pub message: String,
70}
71
72impl fmt::Display for ToolingProductReadErrorV1 {
73 fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(output, "{}: {}", self.code, self.message)
75 }
76}
77
78impl std::error::Error for ToolingProductReadErrorV1 {}
79
80pub fn read_tooling_product_v1(
84 schema: &str,
85 versions: &[u32],
86 bytes: &[u8],
87) -> Result<ToolingProductV1, ToolingProductReadErrorV1> {
88 let negotiation = serde_json::to_vec(&serde_json::json!({
89 "schema": schema,
90 "versions": versions,
91 }))
92 .expect("tooling negotiation request serializes");
93 let request = decode_tooling_schema_negotiation_request_v1(&negotiation).map_err(|error| {
94 ToolingProductReadErrorV1 {
95 code: "L11T001",
96 message: format!("schema negotiation rejected: {error}"),
97 }
98 })?;
99 let accepted =
100 negotiate_tooling_schema_v1(&request).map_err(|error| ToolingProductReadErrorV1 {
101 code: "L11T001",
102 message: format!("schema negotiation rejected: {error}"),
103 })?;
104 if accepted.accepted_version != 1 {
105 return Err(ToolingProductReadErrorV1 {
106 code: "L11T006",
107 message: "accepted tooling schema version does not match the reader".into(),
108 });
109 }
110
111 match accepted.schema.as_str() {
112 WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1 => decode_workspace_snapshot_json_v1(bytes)
113 .map(ToolingProductV1::WorkspaceSnapshot)
114 .map_err(|error| ToolingProductReadErrorV1 {
115 code: "L11T003",
116 message: format!("workspace snapshot fails strict canonical validation: {error:?}"),
117 }),
118 WORKSPACE_GRAPH_TOOLING_SCHEMA_V1 => decode_workspace_graph_json_v1(bytes)
119 .map(ToolingProductV1::WorkspaceGraph)
120 .map_err(|error| ToolingProductReadErrorV1 {
121 code: "L11T003",
122 message: format!("workspace graph fails strict canonical validation: {error:?}"),
123 }),
124 BUILD_TRACE_TOOLING_SCHEMA_V1 => decode_tooling_build_trace_v1(bytes)
125 .map(ToolingProductV1::BuildTrace)
126 .map_err(|error| ToolingProductReadErrorV1 {
127 code: "L11T007",
128 message: format!("build trace fails strict validation: {error:?}"),
129 }),
130 COMPILE_COST_TOOLING_SCHEMA_V1 => decode_tooling_compile_cost_report_v1(bytes)
131 .map(ToolingProductV1::CompileCostReport)
132 .map_err(|error| ToolingProductReadErrorV1 {
133 code: "L11T008",
134 message: format!("compile cost report fails strict validation: {error:?}"),
135 }),
136 ARTIFACT_GRAPH_TOOLING_SCHEMA_V1 => decode_tooling_artifact_graph_v1(bytes)
137 .map(ToolingProductV1::ArtifactGraph)
138 .map_err(|error| ToolingProductReadErrorV1 {
139 code: "L11T010",
140 message: format!("artifact graph fails strict validation: {error:?}"),
141 }),
142 _ => Err(ToolingProductReadErrorV1 {
143 code: "L11T005",
144 message: "no accepted public reader exists for the negotiated schema".into(),
145 }),
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::platform::{
153 derive_workspace_id_v1, CacheLimits, CancellationToken, CompilationOutcome,
154 CompileWorkspaceRequest, CompilerSessionState, RequestedCompilationMode, WorkspaceInput,
155 WorkspaceSource,
156 };
157
158 fn canonical_products() -> (Vec<u8>, Vec<u8>) {
159 let workspace = WorkspaceInput::new(vec![WorkspaceSource {
160 path: "src/ToolingFixture.ts".into(),
161 source: "export const toolingFixture = 1;\n".into(),
162 language: None,
163 }]);
164 let workspace_id = derive_workspace_id_v1(&workspace.configuration).unwrap();
165 let mut session = CompilerSessionState::new(
166 workspace_id,
167 workspace.compiler_contract.clone(),
168 CacheLimits::default(),
169 );
170 let CompilationOutcome::Committed(result) =
171 session.compile_workspace(CompileWorkspaceRequest {
172 workspace,
173 mode: RequestedCompilationMode::Full,
174 cancellation: CancellationToken::new(),
175 })
176 else {
177 panic!("tooling reader fixture compilation must commit");
178 };
179 let snapshot = result.snapshot.to_canonical_json().unwrap();
180 let graph = result.graph.to_canonical_json().unwrap();
181 assert!(!String::from_utf8_lossy(&snapshot).contains("toolingFixture"));
182 assert!(!String::from_utf8_lossy(&graph).contains("toolingFixture"));
183 (snapshot, graph)
184 }
185
186 #[test]
187 fn l11b_reads_only_strict_canonical_l3_product_fixtures() {
188 let (snapshot_bytes, graph_bytes) = canonical_products();
189 let snapshot =
190 read_tooling_product_v1(WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1, &[1], &snapshot_bytes)
191 .unwrap();
192 let graph =
193 read_tooling_product_v1(WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, &[2, 1], &graph_bytes)
194 .unwrap();
195 assert_eq!(snapshot.schema(), WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1);
196 assert_eq!(graph.schema(), WORKSPACE_GRAPH_TOOLING_SCHEMA_V1);
197 assert_eq!(snapshot.version(), 1);
198 assert_eq!(graph.version(), 1);
199 assert_eq!(snapshot.snapshot_id(), graph.snapshot_id());
200 }
201
202 #[test]
203 fn l11b_rejects_unavailable_products_and_noncanonical_bytes() {
204 let (snapshot_bytes, _) = canonical_products();
205 let error = read_tooling_product_v1("presolve.unknown", &[1], &snapshot_bytes).unwrap_err();
206 assert_eq!(error.code, "L11T001");
207 for schema in ["presolve.workspace-configuration", "presolve.watch-event"] {
208 let error = read_tooling_product_v1(schema, &[1], &snapshot_bytes).unwrap_err();
209 assert_eq!(error.code, "L11T005");
210 }
211
212 let mut noncanonical = snapshot_bytes;
213 noncanonical.pop();
214 let error =
215 read_tooling_product_v1(WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1, &[1], &noncanonical)
216 .unwrap_err();
217 assert_eq!(error.code, "L11T003");
218 }
219
220 #[test]
221 fn l11b_reader_order_and_version_validation_are_deterministic() {
222 let (snapshot_bytes, graph_bytes) = canonical_products();
223 let forward = [
224 (
225 WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1,
226 snapshot_bytes.as_slice(),
227 ),
228 (WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, graph_bytes.as_slice()),
229 ]
230 .map(|(schema, bytes)| read_tooling_product_v1(schema, &[1], bytes).unwrap());
231 let reverse = [
232 (WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, graph_bytes.as_slice()),
233 (
234 WORKSPACE_SNAPSHOT_TOOLING_SCHEMA_V1,
235 snapshot_bytes.as_slice(),
236 ),
237 ]
238 .map(|(schema, bytes)| read_tooling_product_v1(schema, &[1], bytes).unwrap());
239 assert_eq!(forward[0].snapshot_id(), reverse[1].snapshot_id());
240 assert_eq!(forward[1].snapshot_id(), reverse[0].snapshot_id());
241
242 let error =
243 read_tooling_product_v1(WORKSPACE_GRAPH_TOOLING_SCHEMA_V1, &[1, 1], &graph_bytes)
244 .unwrap_err();
245 assert_eq!(error.code, "L11T001");
246 }
247}