1use base64::{engine::general_purpose::STANDARD, Engine as _};
9use rusqlite::{params, Connection, TransactionBehavior};
10use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
11pub mod assembly;
12mod generation;
13pub(crate) mod io;
14pub mod materialization;
15mod profile;
16pub(crate) mod read;
17
18use std::collections::{BTreeMap, BTreeSet, HashMap};
19use std::fmt;
20use std::fs::{self, File, OpenOptions};
21use std::io::Write;
22use std::path::{Path, PathBuf};
23use std::sync::{Mutex, OnceLock};
24use std::time::Duration;
25
26pub const PATH_IDENTITY_VERSION: u8 = 1;
29const POINTER_DATABASE: &str = "pointer.sqlite";
30const POINTER_BUSY_TIMEOUT: Duration = Duration::from_millis(5_000);
31const FILE_OPEN_RETRY_TIMEOUT: Duration = Duration::from_millis(5_000);
32
33static HEAD_FINGERPRINTS: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
34
35pub(crate) fn cache_head_fingerprint(root: PathBuf, fingerprint: String) {
36 HEAD_FINGERPRINTS
37 .get_or_init(|| Mutex::new(HashMap::new()))
38 .lock()
39 .unwrap_or_else(std::sync::PoisonError::into_inner)
40 .insert(crate::path_identity::project_scope_key(&root), fingerprint);
41}
42
43pub(crate) fn cached_head_fingerprint(root: &Path) -> Option<String> {
44 HEAD_FINGERPRINTS
45 .get_or_init(|| Mutex::new(HashMap::new()))
46 .lock()
47 .unwrap_or_else(std::sync::PoisonError::into_inner)
48 .get(&crate::path_identity::project_scope_key(root))
49 .cloned()
50}
51
52pub(crate) fn generation_matches_head(generation: &str, head_fingerprint: &str) -> bool {
53 !head_fingerprint.is_empty() && generation.ends_with(head_fingerprint)
54}
55
56pub(crate) fn resolve_derived_path(view_dir: &Path, generation: &str) -> Result<PathBuf> {
57 ViewStore {
58 view_dir: view_dir.to_path_buf(),
59 }
60 .derived_path(generation)
61}
62
63#[derive(Debug)]
65pub enum ViewError {
66 Io(std::io::Error),
67 Sqlite(rusqlite::Error),
68 Json(serde_json::Error),
69 InvalidManifest(String),
70 ManifestAlreadyExists(String),
71 MissingBlob { plane: ArtifactPlane, key: String },
72 MissingTrigram,
73 MissingAlias(String),
74}
75
76impl fmt::Display for ViewError {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::Io(error) => write!(formatter, "view I/O failed: {error}"),
80 Self::Sqlite(error) => write!(formatter, "view SQLite operation failed: {error}"),
81 Self::Json(error) => write!(formatter, "view manifest JSON failed: {error}"),
82 Self::InvalidManifest(message) => write!(formatter, "invalid view manifest: {message}"),
83 Self::ManifestAlreadyExists(generation) => {
84 write!(
85 formatter,
86 "a manifest already exists for generation {generation}"
87 )
88 }
89 Self::MissingBlob { plane, key } => {
90 write!(
91 formatter,
92 "manifest references missing {plane:?} blob {key}"
93 )
94 }
95 Self::MissingTrigram => write!(
96 formatter,
97 "published generation is missing its trigram state"
98 ),
99 Self::MissingAlias(oid) => write!(
100 formatter,
101 "published generation references missing alias {oid}"
102 ),
103 }
104 }
105}
106
107impl std::error::Error for ViewError {
108 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
109 match self {
110 Self::Io(error) => Some(error),
111 Self::Sqlite(error) => Some(error),
112 Self::Json(error) => Some(error),
113 _ => None,
114 }
115 }
116}
117
118impl From<std::io::Error> for ViewError {
119 fn from(error: std::io::Error) -> Self {
120 Self::Io(error)
121 }
122}
123
124impl From<rusqlite::Error> for ViewError {
125 fn from(error: rusqlite::Error) -> Self {
126 Self::Sqlite(error)
127 }
128}
129
130impl From<serde_json::Error> for ViewError {
131 fn from(error: serde_json::Error) -> Self {
132 Self::Json(error)
133 }
134}
135
136pub type Result<T> = std::result::Result<T, ViewError>;
138
139#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
142pub struct ByteString(Vec<u8>);
143
144impl ByteString {
145 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
146 Self(bytes.into())
147 }
148
149 pub fn as_bytes(&self) -> &[u8] {
150 &self.0
151 }
152
153 pub fn into_bytes(self) -> Vec<u8> {
154 self.0
155 }
156}
157
158impl From<Vec<u8>> for ByteString {
159 fn from(bytes: Vec<u8>) -> Self {
160 Self::new(bytes)
161 }
162}
163
164impl From<&[u8]> for ByteString {
165 fn from(bytes: &[u8]) -> Self {
166 Self::new(bytes)
167 }
168}
169
170impl Serialize for ByteString {
171 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
172 where
173 S: Serializer,
174 {
175 match std::str::from_utf8(&self.0) {
176 Ok(text) => serializer.serialize_str(text),
177 Err(_) => {
178 let mut encoded = BTreeMap::new();
179 encoded.insert("b64", STANDARD.encode(&self.0));
180 encoded.serialize(serializer)
181 }
182 }
183 }
184}
185
186impl<'de> Deserialize<'de> for ByteString {
187 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
188 where
189 D: Deserializer<'de>,
190 {
191 let value = serde_json::Value::deserialize(deserializer)?;
192 match value {
193 serde_json::Value::String(text) => Ok(Self::new(text.into_bytes())),
194 serde_json::Value::Object(mut object) if object.len() == 1 => {
195 let encoded = object
196 .remove("b64")
197 .and_then(|value| value.as_str().map(str::to_owned))
198 .ok_or_else(|| {
199 D::Error::custom("byte string object must contain only string b64")
200 })?;
201 STANDARD
202 .decode(encoded)
203 .map(Self::new)
204 .map_err(D::Error::custom)
205 }
206 _ => Err(D::Error::custom(
207 "byte string must be a UTF-8 string or an object with b64",
208 )),
209 }
210 }
211}
212
213#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
215pub struct RelPath(ByteString);
216
217impl RelPath {
218 pub fn new(bytes: impl Into<Vec<u8>>) -> Result<Self> {
220 let bytes = bytes.into();
221 validate_filesystem_rel_path(&bytes)?;
222 Ok(Self(ByteString::new(bytes)))
223 }
224
225 pub fn synthetic(name: &str) -> Result<Self> {
227 if name.is_empty() || name.as_bytes().contains(&0) {
228 return Err(ViewError::InvalidManifest(
229 "synthetic entry names must be non-empty and contain no NUL".to_string(),
230 ));
231 }
232 let mut key = Vec::with_capacity(name.len() + 1);
233 key.push(0);
234 key.extend_from_slice(name.as_bytes());
235 Ok(Self(ByteString::new(key)))
236 }
237
238 pub fn from_os_path(path: &Path) -> Result<Self> {
240 if path.is_absolute() {
241 return Err(ViewError::InvalidManifest(
242 "rel_path must not be absolute".to_string(),
243 ));
244 }
245
246 #[cfg(unix)]
247 let bytes = {
248 use std::os::unix::ffi::OsStrExt as _;
249 path.as_os_str().as_bytes().to_vec()
250 };
251 #[cfg(windows)]
252 let bytes: Vec<u8> = path
253 .as_os_str()
254 .as_encoded_bytes()
255 .iter()
256 .map(|byte| if *byte == b'\\' { b'/' } else { *byte })
257 .collect();
258 #[cfg(all(not(unix), not(windows)))]
259 let bytes = path.as_os_str().as_encoded_bytes().to_vec();
260
261 Self::new(bytes)
262 }
263
264 pub fn as_bytes(&self) -> &[u8] {
265 self.0.as_bytes()
266 }
267
268 pub fn is_synthetic(&self) -> bool {
269 self.0.as_bytes().first() == Some(&0)
270 }
271}
272
273impl Serialize for RelPath {
274 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
275 where
276 S: Serializer,
277 {
278 self.0.serialize(serializer)
279 }
280}
281
282impl<'de> Deserialize<'de> for RelPath {
283 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
284 where
285 D: Deserializer<'de>,
286 {
287 let bytes = ByteString::deserialize(deserializer)?.into_bytes();
288 if bytes.first() == Some(&0) {
289 if bytes.len() == 1 || bytes[1..].contains(&0) {
290 return Err(D::Error::custom("invalid synthetic rel_path"));
291 }
292 Ok(Self(ByteString::new(bytes)))
293 } else {
294 Self::new(bytes).map_err(D::Error::custom)
295 }
296 }
297}
298
299#[derive(Clone, Copy, Debug, Eq, PartialEq)]
301pub enum ArtifactPlane {
302 Semantic,
303 Callgraph,
304}
305
306#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
308pub struct RegularPlanes {
309 pub semantic: Option<String>,
310 pub callgraph: Option<String>,
311}
312
313#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
316pub struct SyntheticPlanes {
317 pub callgraph: String,
318}
319
320#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
323#[serde(tag = "kind", rename_all = "snake_case")]
324pub enum ManifestEntry {
325 Regular {
326 mode: u32,
327 planes: RegularPlanes,
328 resolution_input: bool,
329 },
330 Symlink {
331 target_bytes: ByteString,
332 },
333 Gitlink {
334 oid: String,
335 },
336 Synthetic {
337 name: String,
338 planes: SyntheticPlanes,
339 },
340}
341
342#[derive(Clone, Copy, Debug, Eq, PartialEq)]
344pub enum ManifestEntryKind {
345 Regular,
346 Symlink,
347 Gitlink,
348 Synthetic,
349}
350
351impl ManifestEntry {
352 pub fn kind(&self) -> ManifestEntryKind {
353 match self {
354 Self::Regular { .. } => ManifestEntryKind::Regular,
355 Self::Symlink { .. } => ManifestEntryKind::Symlink,
356 Self::Gitlink { .. } => ManifestEntryKind::Gitlink,
357 Self::Synthetic { .. } => ManifestEntryKind::Synthetic,
358 }
359 }
360}
361
362#[derive(Clone, Debug, Eq, PartialEq)]
364pub struct Manifest {
365 pub path_identity_version: u8,
366 entries: BTreeMap<RelPath, ManifestEntry>,
367}
368
369impl Manifest {
370 pub fn new(entries: impl IntoIterator<Item = (RelPath, ManifestEntry)>) -> Result<Self> {
371 let mut manifest = Self {
372 path_identity_version: PATH_IDENTITY_VERSION,
373 entries: BTreeMap::new(),
374 };
375 for (rel_path, entry) in entries {
376 manifest.insert(rel_path, entry)?;
377 }
378 Ok(manifest)
379 }
380
381 pub fn insert(&mut self, rel_path: RelPath, entry: ManifestEntry) -> Result<()> {
382 validate_member(&rel_path, &entry)?;
383 if self.entries.insert(rel_path, entry).is_some() {
384 return Err(ViewError::InvalidManifest(
385 "a manifest cannot contain the same rel_path twice".to_string(),
386 ));
387 }
388 Ok(())
389 }
390
391 pub fn entries(&self) -> impl Iterator<Item = (&RelPath, &ManifestEntry)> {
392 self.entries.iter()
393 }
394
395 pub fn get(&self, rel_path: &RelPath) -> Option<&ManifestEntry> {
396 self.entries.get(rel_path)
397 }
398
399 pub fn to_json_bytes(&self) -> Result<Vec<u8>> {
400 Ok(serde_json::to_vec(self)?)
401 }
402
403 pub fn from_json_bytes(bytes: &[u8]) -> Result<Self> {
404 Ok(serde_json::from_slice(bytes)?)
405 }
406
407 fn plane_keys(&self) -> impl Iterator<Item = (ArtifactPlane, &str)> {
408 self.entries.values().flat_map(|entry| match entry {
409 ManifestEntry::Regular { planes, .. } => [
410 planes
411 .semantic
412 .as_deref()
413 .map(|key| (ArtifactPlane::Semantic, key)),
414 planes
415 .callgraph
416 .as_deref()
417 .map(|key| (ArtifactPlane::Callgraph, key)),
418 ]
419 .into_iter()
420 .flatten()
421 .collect::<Vec<_>>(),
422 ManifestEntry::Synthetic { planes, .. } => {
423 vec![(ArtifactPlane::Callgraph, planes.callgraph.as_str())]
424 }
425 ManifestEntry::Symlink { .. } | ManifestEntry::Gitlink { .. } => Vec::new(),
426 })
427 }
428}
429
430#[derive(Deserialize, Serialize)]
431struct JsonManifestEntry {
432 rel_path: RelPath,
433 #[serde(flatten)]
434 entry: ManifestEntry,
435}
436
437#[derive(Deserialize, Serialize)]
438struct JsonManifest {
439 path_identity_version: u8,
440 entries: Vec<JsonManifestEntry>,
441}
442
443impl Serialize for Manifest {
444 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
445 where
446 S: Serializer,
447 {
448 let entries = self
449 .entries
450 .iter()
451 .map(|(rel_path, entry)| JsonManifestEntry {
452 rel_path: rel_path.clone(),
453 entry: entry.clone(),
454 })
455 .collect();
456 JsonManifest {
457 path_identity_version: self.path_identity_version,
458 entries,
459 }
460 .serialize(serializer)
461 }
462}
463
464impl<'de> Deserialize<'de> for Manifest {
465 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
466 where
467 D: Deserializer<'de>,
468 {
469 let json = JsonManifest::deserialize(deserializer)?;
470 if json.path_identity_version != PATH_IDENTITY_VERSION {
471 return Err(D::Error::custom(format!(
472 "unsupported path_identity_version {}",
473 json.path_identity_version
474 )));
475 }
476 Self::new(
477 json.entries
478 .into_iter()
479 .map(|member| (member.rel_path, member.entry)),
480 )
481 .map_err(D::Error::custom)
482 }
483}
484
485#[derive(Clone, Debug, Default, Eq, PartialEq)]
489pub struct ClosureRequirements {
490 pub referenced_aliases: BTreeSet<String>,
491}
492
493pub trait PublicationClosure {
495 fn contains_blob(&self, plane: ArtifactPlane, full_key: &str) -> Result<bool>;
496 fn probe_blobs(&self, keys: &[(ArtifactPlane, &str)]) -> Result<()> {
497 for &(plane, key) in keys {
498 if !self.contains_blob(plane, key)? {
499 return Err(ViewError::MissingBlob {
500 plane,
501 key: key.to_owned(),
502 });
503 }
504 }
505 Ok(())
506 }
507 fn trigram_is_present(&self) -> Result<bool>;
508 fn contains_alias(&self, git_oid: &str) -> Result<bool>;
509}
510
511pub fn probe_publication_closure(
514 manifest: &Manifest,
515 requirements: &ClosureRequirements,
516 closure: &impl PublicationClosure,
517) -> Result<()> {
518 closure.probe_blobs(&manifest.plane_keys().collect::<Vec<_>>())?;
519 if !closure.trigram_is_present()? {
520 return Err(ViewError::MissingTrigram);
521 }
522 for oid in &requirements.referenced_aliases {
523 if !closure.contains_alias(oid)? {
524 return Err(ViewError::MissingAlias(oid.clone()));
525 }
526 }
527 Ok(())
528}
529
530#[derive(Clone, Debug)]
535pub struct PublicationArtifacts {
536 pub blob_databases: Vec<PathBuf>,
537 pub derived_database: PathBuf,
538 pub trigram_artifact: PathBuf,
539 pub alias_database: PathBuf,
540}
541
542#[derive(Clone, Debug)]
544pub struct PublicationRequest<'a> {
545 pub generation: &'a str,
546 pub base_generation: Option<&'a str>,
549 pub manifest: &'a Manifest,
550 pub artifacts: PublicationArtifacts,
551 pub closure_requirements: ClosureRequirements,
552}
553
554#[derive(Clone, Copy, Debug, Eq, PartialEq)]
557pub enum PublicationStep {
558 BlobWalCheckpointed,
560 BlobWalFsync,
562 DerivedWalFsynced,
565 DerivedAndTrigramDurable,
566 AliasRowsDurable,
567 ClosureProbed,
568 ManifestFileWritten,
569 ManifestParentFsynced,
570 PointerCas,
571 PointerCheckpointed,
572 PointerDatabaseFsynced,
573 PointerDirectoryFsynced,
574}
575
576pub trait PublicationObserver {
577 fn reached(&self, step: PublicationStep);
578}
579
580#[derive(Clone, Debug, Eq, PartialEq)]
582pub enum PublishOutcome {
583 Published,
584 Conflict { current_generation: Option<String> },
585}
586
587#[derive(Debug)]
590pub struct PreparedPublication {
591 store: ViewStore,
592 generation: String,
593 base_generation: Option<String>,
594 pointer: Connection,
595}
596
597impl PreparedPublication {
598 pub fn commit(mut self) -> Result<PublishOutcome> {
599 let transaction = self
602 .pointer
603 .transaction_with_behavior(TransactionBehavior::Immediate)?;
604 let updated = transaction.execute(
605 "UPDATE pointer SET generation = ?1 WHERE generation = ?2",
606 params![
607 self.generation,
608 self.base_generation.as_deref().unwrap_or_default()
609 ],
610 )?;
611 let outcome = if updated == 1 {
612 PublishOutcome::Published
613 } else {
614 let current: String = transaction.query_row(
615 "SELECT generation FROM pointer WHERE singleton = 1",
616 [],
617 |row| row.get(0),
618 )?;
619 PublishOutcome::Conflict {
620 current_generation: (!current.is_empty()).then_some(current),
621 }
622 };
623 transaction.commit()?;
624 Ok(outcome)
625 }
626
627 fn commit_with_observer(
628 self,
629 observer: Option<&dyn PublicationObserver>,
630 ) -> Result<PublishOutcome> {
631 self.store
632 .commit_prepared(&self.generation, self.base_generation.as_deref(), observer)
633 }
634}
635
636#[derive(Clone, Debug, Eq, PartialEq)]
638pub struct PointerPragmas {
639 pub journal_mode: String,
640 pub synchronous: i64,
641 pub busy_timeout_ms: i64,
642 pub foreign_keys: i64,
643}
644
645#[derive(Clone, Debug)]
647pub struct ViewStore {
648 view_dir: PathBuf,
649}
650
651impl ViewStore {
652 pub fn open(storage: impl AsRef<Path>, project_scope_key: &str) -> Result<Self> {
655 validate_scope_key(project_scope_key)?;
656 let view_dir = storage.as_ref().join("views").join(project_scope_key);
657 fs::create_dir_all(&view_dir)?;
658 let store = Self { view_dir };
659 store.initialize_pointer()?;
660 Ok(store)
661 }
662
663 pub fn view_dir(&self) -> &Path {
664 &self.view_dir
665 }
666
667 pub fn pointer_path(&self) -> PathBuf {
668 self.view_dir.join(POINTER_DATABASE)
669 }
670
671 pub fn manifest_path(&self, generation: &str) -> Result<PathBuf> {
672 validate_generation(generation)?;
673 Ok(self.view_dir.join(format!("manifest-{generation}.json")))
674 }
675
676 pub fn current_generation(&self) -> Result<Option<String>> {
677 let connection = self.open_pointer_connection()?;
678 let generation: String = connection.query_row(
679 "SELECT generation FROM pointer WHERE singleton = 1",
680 [],
681 |row| row.get(0),
682 )?;
683 Ok((!generation.is_empty()).then_some(generation))
684 }
685
686 pub fn pointer_pragmas(&self) -> Result<PointerPragmas> {
690 let connection = self.open_pointer_connection()?;
691 Ok(PointerPragmas {
692 journal_mode: connection.query_row("PRAGMA journal_mode", [], |row| row.get(0))?,
693 synchronous: connection.query_row("PRAGMA synchronous", [], |row| row.get(0))?,
694 busy_timeout_ms: connection.query_row("PRAGMA busy_timeout", [], |row| row.get(0))?,
695 foreign_keys: connection.query_row("PRAGMA foreign_keys", [], |row| row.get(0))?,
696 })
697 }
698
699 pub fn load_manifest(&self, generation: &str) -> Result<Manifest> {
700 Ok(Manifest::from_json_bytes(&fs::read(
701 self.manifest_path(generation)?,
702 )?)?)
703 }
704
705 pub fn publish(
709 &self,
710 request: &PublicationRequest<'_>,
711 closure: &impl PublicationClosure,
712 ) -> Result<PublishOutcome> {
713 self.publish_with_observer(request, closure, None)
714 }
715
716 pub fn publish_with_observer(
717 &self,
718 request: &PublicationRequest<'_>,
719 closure: &impl PublicationClosure,
720 observer: Option<&dyn PublicationObserver>,
721 ) -> Result<PublishOutcome> {
722 let prepared = self.prepare_with_observer(request, closure, observer)?;
723 prepared.commit_with_observer(observer)
724 }
725
726 pub fn prepare_with_observer(
729 &self,
730 request: &PublicationRequest<'_>,
731 closure: &impl PublicationClosure,
732 observer: Option<&dyn PublicationObserver>,
733 ) -> Result<PreparedPublication> {
734 self.prepare_inner(request, closure, observer, None)
735 }
736
737 pub(super) fn prepare_with_reused_derived(
738 &self,
739 request: &PublicationRequest<'_>,
740 closure: &impl PublicationClosure,
741 previous: Option<&Manifest>,
742 ) -> Result<PreparedPublication> {
743 self.prepare_inner(request, closure, None, previous)
744 }
745
746 fn prepare_inner(
747 &self,
748 request: &PublicationRequest<'_>,
749 closure: &impl PublicationClosure,
750 observer: Option<&dyn PublicationObserver>,
751 reused: Option<&Manifest>,
752 ) -> Result<PreparedPublication> {
753 validate_generation(request.generation)?;
754 if request.manifest.path_identity_version != PATH_IDENTITY_VERSION {
755 return Err(ViewError::InvalidManifest(
756 "manifest has an unsupported path identity version".to_string(),
757 ));
758 }
759 let manifest_path = self.manifest_path(request.generation)?;
760 if manifest_path.exists() {
761 return Err(ViewError::ManifestAlreadyExists(
762 request.generation.to_string(),
763 ));
764 }
765
766 let mut timing = profile::PublicationTiming::new(&self.view_dir);
767 let blob_durability_barrier = crate::blob_store::publication_durability_barrier();
768 timing.phase("closure_lock");
769 for path in &request.artifacts.blob_databases {
770 if !crate::blob_store::blob_database_needs_durability(path) {
771 continue;
772 }
773 checkpoint_and_sync_database(path, observer, true)?;
774 crate::blob_store::mark_blob_database_durable(path);
775 }
776
777 timing.phase("blob_durability");
778 if reused.is_none() {
782 sync_database_wal_without_checkpoint(&request.artifacts.derived_database, observer)?;
783 }
784 timing.phase("derived_durability");
785 sync_file_and_parent(&request.artifacts.trigram_artifact)?;
786 observe(observer, PublicationStep::DerivedAndTrigramDurable);
787 timing.phase("trigram_durability");
788
789 if reused.is_none() {
790 checkpoint_and_sync_database(&request.artifacts.alias_database, observer, false)?;
791 }
792 observe(observer, PublicationStep::AliasRowsDurable);
793 timing.phase("alias_durability");
794
795 if let Some(previous) = reused {
796 let old_keys = previous
799 .plane_keys()
800 .map(|(plane, key)| (plane == ArtifactPlane::Semantic, key))
801 .collect::<BTreeSet<_>>();
802 let new_keys = request
803 .manifest
804 .plane_keys()
805 .filter(|(plane, key)| {
806 !old_keys.contains(&(*plane == ArtifactPlane::Semantic, *key))
807 })
808 .collect::<Vec<_>>();
809 closure.probe_blobs(&new_keys)?;
810 if !closure.trigram_is_present()? {
811 return Err(ViewError::MissingTrigram);
812 }
813 } else {
814 probe_publication_closure(request.manifest, &request.closure_requirements, closure)?;
815 }
816 observe(observer, PublicationStep::ClosureProbed);
817 drop(blob_durability_barrier);
818 timing.phase("closure_probe");
819
820 write_manifest_once(&manifest_path, request.manifest)?;
821 observe(observer, PublicationStep::ManifestFileWritten);
822 sync_directory(&self.view_dir)?;
823 observe(observer, PublicationStep::ManifestParentFsynced);
824 timing.phase("manifest_durability");
825
826 let pointer = self.open_pointer_connection()?;
827 pointer.pragma_update(None, "synchronous", "FULL")?;
828 pointer.busy_timeout(Duration::from_millis(20))?;
829 sync_directory(&self.view_dir)?;
830 timing.phase("pointer_prepare");
831 Ok(PreparedPublication {
832 pointer,
833 store: self.clone(),
834 generation: request.generation.to_owned(),
835 base_generation: request.base_generation.map(str::to_owned),
836 })
837 }
838
839 fn commit_prepared(
840 &self,
841 generation: &str,
842 base_generation: Option<&str>,
843 observer: Option<&dyn PublicationObserver>,
844 ) -> Result<PublishOutcome> {
845 let outcome =
846 self.compare_and_swap_pointer(generation, base_generation.unwrap_or_default())?;
847 observe(observer, PublicationStep::PointerCas);
848
849 if outcome == PublishOutcome::Published {
850 checkpoint_pointer_after_cas(&self.pointer_path())?;
851 observe(observer, PublicationStep::PointerCheckpointed);
852 sync_file(&self.pointer_path())?;
853 observe(observer, PublicationStep::PointerDatabaseFsynced);
854 sync_directory(&self.view_dir)?;
855 observe(observer, PublicationStep::PointerDirectoryFsynced);
856 }
857 Ok(outcome)
858 }
859
860 fn initialize_pointer(&self) -> Result<()> {
861 let connection = self.open_pointer_connection()?;
862 connection.execute_batch(
863 "BEGIN IMMEDIATE;
864 CREATE TABLE IF NOT EXISTS pointer (
865 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
866 generation TEXT NOT NULL
867 );
868 INSERT OR IGNORE INTO pointer (singleton, generation) VALUES (1, '');
869 COMMIT;",
870 )?;
871 Ok(())
872 }
873
874 fn open_pointer_connection(&self) -> Result<Connection> {
875 let connection = Connection::open(self.pointer_path())?;
876 configure_connection(&connection)?;
877 Ok(connection)
878 }
879
880 fn compare_and_swap_pointer(
881 &self,
882 generation: &str,
883 base_generation: &str,
884 ) -> Result<PublishOutcome> {
885 let mut connection = self.open_pointer_connection()?;
886 let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?;
887 let updated = transaction.execute(
888 "UPDATE pointer SET generation = ?1 WHERE generation = ?2",
889 params![generation, base_generation],
890 )?;
891 if updated == 1 {
892 transaction.commit()?;
893 Ok(PublishOutcome::Published)
894 } else {
895 let current_generation: String = transaction.query_row(
896 "SELECT generation FROM pointer WHERE singleton = 1",
897 [],
898 |row| row.get(0),
899 )?;
900 transaction.commit()?;
901 Ok(PublishOutcome::Conflict {
902 current_generation: (!current_generation.is_empty()).then_some(current_generation),
903 })
904 }
905 }
906}
907
908fn validate_filesystem_rel_path(bytes: &[u8]) -> Result<()> {
909 if bytes.is_empty()
910 || bytes.first() == Some(&b'/')
911 || bytes.contains(&0)
912 || bytes
913 .split(|byte| *byte == b'/')
914 .any(|part| part.is_empty() || part == b"." || part == b"..")
915 {
916 return Err(ViewError::InvalidManifest(
917 "rel_path must be a non-empty relative slash-separated byte path without NUL or traversal"
918 .to_string(),
919 ));
920 }
921 Ok(())
922}
923
924fn validate_member(rel_path: &RelPath, entry: &ManifestEntry) -> Result<()> {
925 match entry {
926 ManifestEntry::Synthetic { name, .. } => {
927 if rel_path.as_bytes()
928 != [b'\0']
929 .into_iter()
930 .chain(name.as_bytes().iter().copied())
931 .collect::<Vec<_>>()
932 {
933 return Err(ViewError::InvalidManifest(
934 "synthetic entries must use the reserved \\0<name> rel_path key".to_string(),
935 ));
936 }
937 }
938 ManifestEntry::Regular { mode, .. } => {
939 if rel_path.is_synthetic() {
940 return Err(ViewError::InvalidManifest(
941 "only synthetic entries may use a leading NUL rel_path".to_string(),
942 ));
943 }
944 if !matches!(*mode, 0o100644 | 0o100755) {
945 return Err(ViewError::InvalidManifest(
946 "regular manifest entries must use mode 100644 or 100755".to_string(),
947 ));
948 }
949 }
950 ManifestEntry::Symlink { .. } | ManifestEntry::Gitlink { .. }
951 if rel_path.is_synthetic() =>
952 {
953 return Err(ViewError::InvalidManifest(
954 "only synthetic entries may use a leading NUL rel_path".to_string(),
955 ));
956 }
957 ManifestEntry::Symlink { .. } | ManifestEntry::Gitlink { .. } => {}
958 }
959 Ok(())
960}
961
962fn validate_generation(generation: &str) -> Result<()> {
963 if generation.is_empty()
964 || generation.contains(['/', '\\', '\0'])
965 || generation == "."
966 || generation == ".."
967 {
968 return Err(ViewError::InvalidManifest(
969 "generation must be a non-empty file-name component".to_string(),
970 ));
971 }
972 Ok(())
973}
974
975fn validate_scope_key(project_scope_key: &str) -> Result<()> {
976 if project_scope_key.is_empty()
977 || project_scope_key.contains(['/', '\\', '\0'])
978 || project_scope_key == "."
979 || project_scope_key == ".."
980 {
981 return Err(ViewError::InvalidManifest(
982 "project_scope_key must be a non-empty directory-name component".to_string(),
983 ));
984 }
985 Ok(())
986}
987
988fn configure_connection(connection: &Connection) -> Result<()> {
989 connection.busy_timeout(POINTER_BUSY_TIMEOUT)?;
990 crate::blob_store::retry_while_busy(POINTER_BUSY_TIMEOUT, || {
996 connection.pragma_update(None, "journal_mode", "WAL")
997 })?;
998 connection.pragma_update(None, "synchronous", "NORMAL")?;
999 connection.pragma_update(None, "foreign_keys", "OFF")?;
1000 Ok(())
1001}
1002
1003fn sync_database_wal_without_checkpoint(
1004 path: &Path,
1005 observer: Option<&dyn PublicationObserver>,
1006) -> Result<()> {
1007 if !path.is_file() {
1008 return Err(ViewError::InvalidManifest(format!(
1009 "durability input is not a SQLite file: {}",
1010 path.display()
1011 )));
1012 }
1013 sync_file(path)?;
1014 let wal_path = PathBuf::from(format!("{}-wal", path.display()));
1015 match open_file_for_sync(&wal_path) {
1016 Ok(file) => file.sync_all()?,
1017 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1018 Err(error) => return Err(ViewError::Io(error)),
1019 }
1020 sync_parent(path)?;
1021 observe(observer, PublicationStep::DerivedWalFsynced);
1022 Ok(())
1023}
1024
1025fn checkpoint_and_sync_database(
1026 path: &Path,
1027 observer: Option<&dyn PublicationObserver>,
1028 observe_blob_database: bool,
1029) -> Result<()> {
1030 if !path.is_file() {
1031 return Err(ViewError::InvalidManifest(format!(
1032 "durability input is not a SQLite file: {}",
1033 path.display()
1034 )));
1035 }
1036 let connection = Connection::open(path)?;
1037 configure_connection(&connection)?;
1038 connection.execute_batch("PRAGMA wal_checkpoint(PASSIVE);")?;
1039 if observe_blob_database {
1040 observe(observer, PublicationStep::BlobWalCheckpointed);
1041 }
1042 drop(connection);
1043 sync_file(path)?;
1044 let wal_path = PathBuf::from(format!("{}-wal", path.display()));
1051 match open_file_for_sync(&wal_path) {
1052 Ok(file) => file.sync_all()?,
1053 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1054 Err(error) => return Err(ViewError::Io(error)),
1055 }
1056 sync_parent(path)?;
1057 if observe_blob_database {
1058 observe(observer, PublicationStep::BlobWalFsync);
1059 }
1060 Ok(())
1061}
1062
1063fn checkpoint_pointer_after_cas(path: &Path) -> Result<()> {
1064 let connection = Connection::open(path)?;
1065 configure_connection(&connection)?;
1066 connection.execute_batch("PRAGMA wal_checkpoint(PASSIVE);")?;
1067 Ok(())
1068}
1069
1070fn write_manifest_once(path: &Path, manifest: &Manifest) -> Result<()> {
1071 let parent = path.parent().ok_or_else(|| {
1072 ViewError::InvalidManifest("manifest path must have a parent directory".to_string())
1073 })?;
1074 let temporary = parent.join(format!(
1075 ".{}.tmp.{}.{}",
1076 path.file_name()
1077 .and_then(|name| name.to_str())
1078 .unwrap_or("manifest"),
1079 std::process::id(),
1080 std::time::SystemTime::now()
1081 .duration_since(std::time::UNIX_EPOCH)
1082 .unwrap_or_default()
1083 .as_nanos()
1084 ));
1085 let write_result = (|| -> Result<()> {
1086 let mut file = OpenOptions::new()
1087 .create_new(true)
1088 .write(true)
1089 .open(&temporary)?;
1090 file.write_all(&manifest.to_json_bytes()?)?;
1091 file.write_all(b"\n")?;
1092 file.sync_all()?;
1093 drop(file);
1094 fs::hard_link(&temporary, path).map_err(|error| {
1095 if error.kind() == std::io::ErrorKind::AlreadyExists {
1096 ViewError::ManifestAlreadyExists(
1097 path.file_stem()
1098 .and_then(|name| name.to_str())
1099 .unwrap_or("unknown")
1100 .to_string(),
1101 )
1102 } else {
1103 ViewError::Io(error)
1104 }
1105 })?;
1106 sync_file(path)?;
1109 Ok(())
1110 })();
1111 let _ = fs::remove_file(&temporary);
1112 write_result
1113}
1114
1115fn sync_file_and_parent(path: &Path) -> Result<()> {
1116 sync_file(path)?;
1117 sync_parent(path)
1118}
1119
1120fn sync_file(path: &Path) -> Result<()> {
1121 open_file_for_sync(path)?.sync_all()?;
1122 Ok(())
1123}
1124
1125fn open_file_for_sync(path: &Path) -> std::io::Result<File> {
1126 retry_while_windows_file_busy(FILE_OPEN_RETRY_TIMEOUT, || open_file_for_sync_once(path))
1130}
1131
1132fn open_file_for_sync_once(path: &Path) -> std::io::Result<File> {
1133 #[cfg(windows)]
1134 {
1135 OpenOptions::new().write(true).open(path)
1138 }
1139 #[cfg(not(windows))]
1140 {
1141 File::open(path)
1142 }
1143}
1144
1145fn retry_while_windows_file_busy<T>(
1146 budget: Duration,
1147 mut operation: impl FnMut() -> std::io::Result<T>,
1148) -> std::io::Result<T> {
1149 let deadline = std::time::Instant::now() + budget;
1150 let mut backoff = Duration::from_millis(1);
1151 loop {
1152 match operation() {
1153 Err(error) if is_windows_file_busy(&error) && std::time::Instant::now() < deadline => {
1154 std::thread::sleep(backoff);
1155 backoff = (backoff * 2).min(Duration::from_millis(50));
1156 }
1157 result => return result,
1158 }
1159 }
1160}
1161
1162fn is_windows_file_busy(error: &std::io::Error) -> bool {
1163 #[cfg(any(windows, test))]
1166 if error.kind() == std::io::ErrorKind::PermissionDenied {
1167 return true;
1168 }
1169 #[cfg(windows)]
1170 if matches!(error.raw_os_error(), Some(5 | 32)) {
1171 return true;
1172 }
1173 #[cfg(not(any(windows, test)))]
1174 let _ = error;
1175 false
1176}
1177
1178fn sync_parent(path: &Path) -> Result<()> {
1179 let parent = path.parent().ok_or_else(|| {
1180 ViewError::InvalidManifest("durability input must have a parent directory".to_string())
1181 })?;
1182 sync_directory(parent)
1183}
1184
1185#[cfg(unix)]
1186fn sync_directory(path: &Path) -> Result<()> {
1187 File::open(path)?.sync_all()?;
1188 Ok(())
1189}
1190
1191#[cfg(not(unix))]
1192fn sync_directory(_path: &Path) -> Result<()> {
1193 Ok(())
1196}
1197
1198fn observe(observer: Option<&dyn PublicationObserver>, step: PublicationStep) {
1199 if let Some(observer) = observer {
1200 observer.reached(step);
1201 }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206 use super::*;
1207
1208 #[test]
1217 fn concurrent_artifact_checkpoints_never_see_busy() {
1218 let mut failures = Vec::new();
1219 for round in 0..150 {
1220 let dir = tempfile::tempdir().expect("tempdir");
1221 let artifact = dir.path().join("artifact.sqlite");
1222 Connection::open(&artifact)
1223 .expect("artifact")
1224 .execute_batch("CREATE TABLE t (x INTEGER);")
1225 .expect("schema");
1226 let handles: Vec<_> = (0..8)
1227 .map(|_| {
1228 let artifact = artifact.clone();
1229 std::thread::spawn(move || checkpoint_and_sync_database(&artifact, None, false))
1230 })
1231 .collect();
1232 for handle in handles {
1233 if let Err(error) = handle.join().expect("opener thread") {
1234 failures.push(format!("round {round}: {error}"));
1235 }
1236 }
1237 }
1238 assert!(
1239 failures.is_empty(),
1240 "concurrent artifact checkpoints must wait out the WAL switch: {failures:?}"
1241 );
1242 }
1243
1244 #[test]
1245 fn sync_file_flushes_an_existing_artifact() {
1246 let dir = tempfile::tempdir().expect("tempdir");
1247 let artifact = dir.path().join("artifact.bin");
1248 fs::write(&artifact, b"durable").expect("artifact");
1249
1250 sync_file(&artifact).expect("flush pre-existing artifact");
1251 }
1252
1253 #[test]
1254 fn windows_file_busy_retry_recovers_from_injected_permission_denied() {
1255 let mut attempts = 0;
1256 let result = retry_while_windows_file_busy(Duration::from_millis(100), || {
1257 attempts += 1;
1258 if attempts == 1 {
1259 Err(std::io::Error::new(
1260 std::io::ErrorKind::PermissionDenied,
1261 "injected Windows sharing failure",
1262 ))
1263 } else {
1264 Ok("opened")
1265 }
1266 });
1267
1268 assert_eq!(result.unwrap(), "opened");
1269 assert_eq!(attempts, 2);
1270 }
1271
1272 #[test]
1273 fn byte_string_uses_base64_only_when_utf8_cannot_represent_the_path() {
1274 assert_eq!(
1275 serde_json::to_string(&ByteString::from(b"src/lib.rs".as_slice())).unwrap(),
1276 "\"src/lib.rs\""
1277 );
1278 assert_eq!(
1279 serde_json::to_string(&ByteString::from(b"bad-\xff".as_slice())).unwrap(),
1280 "{\"b64\":\"YmFkLf8=\"}"
1281 );
1282 }
1283
1284 #[test]
1285 fn synthetic_paths_sort_before_filesystem_paths() {
1286 let synthetic = RelPath::synthetic("global-gitignore").unwrap();
1287 let regular = RelPath::new(b"src/main.rs".to_vec()).unwrap();
1288 assert!(synthetic < regular);
1289 }
1290}