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