1use crate::range::{ContentRange, Range};
6use crate::repository::{Error, RepoProvider, RepoStorage, Resource};
7use crate::util::file_stream;
8use anyhow::{Context as _, Result, anyhow};
9use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
10use delivery_blob::DeliveryBlobType;
11use fuchsia_async as fasync;
12use fuchsia_merkle::Hash;
13use futures::future::BoxFuture;
14use futures::{AsyncRead, FutureExt as _};
15use log::warn;
16use std::collections::BTreeSet;
17use std::fs::{self, DirBuilder};
18use std::io::{Seek as _, SeekFrom};
19use std::os::unix::fs::MetadataExt;
20use std::time::SystemTime;
21use tempfile::{NamedTempFile, TempPath};
22use tuf::metadata::{MetadataPath, MetadataVersion, TargetPath};
23use tuf::pouf::Pouf1;
24use tuf::repository::{
25 FileSystemRepository as TufFileSystemRepository,
26 FileSystemRepositoryBuilder as TufFileSystemRepositoryBuilder,
27 RepositoryProvider as TufRepositoryProvider, RepositoryStorage as TufRepositoryStorage,
28};
29
30#[cfg(not(target_os = "fuchsia"))]
31use {
32 crate::repository::RepositorySpec,
33 futures::{Stream, StreamExt as _, stream::BoxStream},
34 notify::{RecursiveMode, Watcher as _, recommended_watcher},
35 std::{
36 ffi::OsStr,
37 pin::Pin,
38 task::{Context, Poll},
39 },
40};
41
42#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
44pub enum CopyMode {
45 #[default]
50 Copy,
51
52 CopyOverwrite,
57
58 HardLink,
60}
61
62pub struct FileSystemRepositoryBuilder {
64 metadata_repo_path: Utf8PathBuf,
65 blob_repo_path: Utf8PathBuf,
66 copy_mode: CopyMode,
67 aliases: BTreeSet<String>,
68 delivery_blob_type: DeliveryBlobType,
69}
70
71impl FileSystemRepositoryBuilder {
72 pub fn new(metadata_repo_path: Utf8PathBuf, blob_repo_path: Utf8PathBuf) -> Self {
75 FileSystemRepositoryBuilder {
76 metadata_repo_path,
77 blob_repo_path,
78 copy_mode: CopyMode::Copy,
79 aliases: BTreeSet::new(),
80 delivery_blob_type: DeliveryBlobType::Type1,
81 }
82 }
83
84 pub fn copy_mode(mut self, copy_mode: CopyMode) -> Self {
86 self.copy_mode = copy_mode;
87 self
88 }
89
90 pub fn alias(mut self, alias: String) -> Self {
92 self.aliases.insert(alias);
93 self
94 }
95
96 pub fn aliases(mut self, aliases: impl IntoIterator<Item = String>) -> Self {
98 for alias in aliases {
99 self = self.alias(alias);
100 }
101 self
102 }
103
104 pub fn delivery_blob_type(mut self, delivery_blob_type: DeliveryBlobType) -> Self {
106 self.delivery_blob_type = delivery_blob_type;
107 self
108 }
109
110 pub fn blob_repo_path(mut self, blob_repo_path: Utf8PathBuf) -> Self {
112 self.blob_repo_path = blob_repo_path;
113 self
114 }
115
116 pub fn build(self) -> FileSystemRepository {
118 FileSystemRepository {
119 metadata_repo_path: self.metadata_repo_path.clone(),
120 blob_repo_path: self.blob_repo_path,
121 copy_mode: self.copy_mode,
122 aliases: self.aliases,
123 delivery_blob_type: self.delivery_blob_type,
124 tuf_repo: TufFileSystemRepositoryBuilder::new(self.metadata_repo_path)
125 .targets_prefix("targets")
126 .build(),
127 }
128 }
129}
130
131#[derive(Debug)]
133pub struct FileSystemRepository {
134 metadata_repo_path: Utf8PathBuf,
135 blob_repo_path: Utf8PathBuf,
136 copy_mode: CopyMode,
137 aliases: BTreeSet<String>,
138 delivery_blob_type: DeliveryBlobType,
139 tuf_repo: TufFileSystemRepository<Pouf1>,
140}
141
142impl FileSystemRepository {
143 pub fn builder(
145 metadata_repo_path: Utf8PathBuf,
146 blob_repo_path: Utf8PathBuf,
147 ) -> FileSystemRepositoryBuilder {
148 FileSystemRepositoryBuilder::new(metadata_repo_path, blob_repo_path)
149 }
150
151 pub fn new(metadata_repo_path: Utf8PathBuf, blob_repo_path: Utf8PathBuf) -> Self {
153 Self::builder(metadata_repo_path, blob_repo_path).build()
154 }
155
156 pub fn blob_repo_path(&self) -> &Utf8PathBuf {
157 &self.blob_repo_path
158 }
159
160 fn fetch<'a>(
161 &'a self,
162 repo_path: &Utf8Path,
163 resource_path: &str,
164 range: Range,
165 ) -> BoxFuture<'a, Result<Resource, Error>> {
166 let file_path = sanitize_path(repo_path, resource_path);
167 async move {
168 let file_path = file_path?;
169 let mut file = std::fs::File::open(&file_path).map_err(|err| {
170 if err.kind() == std::io::ErrorKind::NotFound {
171 Error::NotFound
172 } else {
173 Error::Io(err)
174 }
175 })?;
176
177 let total_len = file.metadata().map_err(Error::Io)?.len();
178
179 let content_range = match range {
180 Range::Full => ContentRange::Full { complete_len: total_len },
181 Range::Inclusive { first_byte_pos, last_byte_pos } => {
182 if first_byte_pos > last_byte_pos
183 || first_byte_pos >= total_len
184 || last_byte_pos >= total_len
185 {
186 return Err(Error::RangeNotSatisfiable);
187 }
188
189 file.seek(SeekFrom::Start(first_byte_pos)).map_err(Error::Io)?;
190
191 ContentRange::Inclusive {
192 first_byte_pos,
193 last_byte_pos,
194 complete_len: total_len,
195 }
196 }
197 Range::From { first_byte_pos } => {
198 if first_byte_pos >= total_len {
199 return Err(Error::RangeNotSatisfiable);
200 }
201
202 file.seek(SeekFrom::Start(first_byte_pos)).map_err(Error::Io)?;
203
204 ContentRange::Inclusive {
205 first_byte_pos,
206 last_byte_pos: total_len - 1,
207 complete_len: total_len,
208 }
209 }
210 Range::Suffix { len } => {
211 if len > total_len {
212 return Err(Error::RangeNotSatisfiable);
213 }
214 let start = total_len - len;
215 file.seek(SeekFrom::Start(start)).map_err(Error::Io)?;
216
217 ContentRange::Inclusive {
218 first_byte_pos: start,
219 last_byte_pos: total_len - 1,
220 complete_len: total_len,
221 }
222 }
223 };
224
225 let content_len = content_range.content_len();
226
227 Ok(Resource {
228 content_range,
229 stream: Box::pin(file_stream(content_len, file, Some(file_path))),
230 })
231 }
232 .boxed()
233 }
234}
235
236impl RepoProvider for FileSystemRepository {
237 #[cfg(not(target_os = "fuchsia"))]
238 fn spec(&self) -> RepositorySpec {
239 RepositorySpec::FileSystem {
240 metadata_repo_path: self.metadata_repo_path.clone(),
241 blob_repo_path: self.blob_repo_path.clone(),
242 aliases: self.aliases.clone(),
243 }
244 }
245
246 fn aliases(&self) -> &BTreeSet<String> {
247 &self.aliases
248 }
249
250 fn fetch_metadata_range<'a>(
251 &'a self,
252 resource_path: &str,
253 range: Range,
254 ) -> BoxFuture<'a, Result<Resource, Error>> {
255 self.fetch(&self.metadata_repo_path, resource_path, range)
256 }
257
258 fn fetch_blob_range<'a>(
259 &'a self,
260 resource_path: &str,
261 range: Range,
262 ) -> BoxFuture<'a, Result<Resource, Error>> {
263 self.fetch(&self.blob_repo_path, resource_path, range)
264 }
265
266 #[cfg(not(target_os = "fuchsia"))]
267 fn supports_watch(&self) -> bool {
268 true
269 }
270
271 #[cfg(not(target_os = "fuchsia"))]
272 fn watch(&self) -> Result<BoxStream<'static, ()>> {
273 let (mut sender, receiver) = futures::channel::mpsc::channel(1);
276
277 let mut watcher = recommended_watcher(move |event: notify::Result<notify::Event>| {
278 let event = match event {
279 Ok(event) => event,
280 Err(err) => {
281 warn!("error receving notify event: {}", err);
282 return;
283 }
284 };
285
286 if !matches!(
287 event.kind,
288 notify::EventKind::Create(_)
289 | notify::EventKind::Modify(_)
290 | notify::EventKind::Remove(_)
291 ) {
292 return;
293 }
294
295 let timestamp_name = OsStr::new("timestamp.json");
297 if event.paths.iter().any(|p| p.file_name() == Some(timestamp_name))
298 && let Err(e) = sender.try_send(())
299 {
300 if e.is_full() {
301 } else if !e.is_disconnected() {
305 warn!("Error sending event: {:?}", e);
306 }
307 }
308 })?;
309
310 watcher.watch(self.metadata_repo_path.as_std_path(), RecursiveMode::NonRecursive)?;
313
314 Ok(WatchStream { _watcher: watcher, receiver }.boxed())
315 }
316
317 fn blob_modification_time<'a>(
318 &'a self,
319 path: &str,
320 ) -> BoxFuture<'a, Result<Option<SystemTime>>> {
321 let file_path = sanitize_path(&self.blob_repo_path, path);
322 async move {
323 let file_path = file_path?;
324 Ok(Some(fs::metadata(&file_path)?.modified()?))
325 }
326 .boxed()
327 }
328
329 fn blob_type(&self) -> DeliveryBlobType {
330 self.delivery_blob_type
331 }
332}
333
334impl TufRepositoryProvider<Pouf1> for FileSystemRepository {
335 fn fetch_metadata<'a>(
336 &'a self,
337 meta_path: &MetadataPath,
338 version: MetadataVersion,
339 ) -> BoxFuture<'a, tuf::Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
340 self.tuf_repo.fetch_metadata(meta_path, version)
341 }
342
343 fn fetch_target<'a>(
344 &'a self,
345 target_path: &TargetPath,
346 ) -> BoxFuture<'a, tuf::Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
347 self.tuf_repo.fetch_target(target_path)
348 }
349}
350
351impl TufRepositoryStorage<Pouf1> for FileSystemRepository {
352 fn store_metadata<'a>(
353 &'a self,
354 meta_path: &MetadataPath,
355 version: MetadataVersion,
356 metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a),
357 ) -> BoxFuture<'a, tuf::Result<()>> {
358 self.tuf_repo.store_metadata(meta_path, version, metadata)
359 }
360
361 fn store_target<'a>(
362 &'a self,
363 target_path: &TargetPath,
364 target: &'a mut (dyn AsyncRead + Send + Unpin + 'a),
365 ) -> BoxFuture<'a, tuf::Result<()>> {
366 self.tuf_repo.store_target(target_path, target)
367 }
368}
369
370impl RepoStorage for FileSystemRepository {
371 fn store_blob<'a>(
372 &'a self,
373 hash: &Hash,
374 len: u64,
375 src: &Utf8Path,
376 ) -> BoxFuture<'a, Result<()>> {
377 let src = src.to_path_buf();
378 let hash_str = hash.to_string();
379 let hash = *hash;
380
381 async move {
382 let src_metadata = fs::metadata(&src)?;
383 if src_metadata.len() != len {
384 return Err(anyhow!(BlobSizeMismatchError {
385 hash,
386 path: src.clone(),
387 manifest_size: len,
388 file_size: src_metadata.len(),
389 }));
390 }
391
392 let dst = sanitize_path(
393 &self.blob_repo_path,
394 &format!("{}/{hash_str}", u32::from(self.delivery_blob_type)),
395 )?;
396 let existing_len = match fs::File::open(&dst) {
397 Ok(file) => {
398 if let Ok(len) = delivery_blob::decompressed_size_from_reader(file) {
399 Some(len)
400 } else {
401 warn!("corrupt delivery blob found at {dst}, overwriting");
404 None
405 }
406 }
407 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
408 Err(e) => return Err(anyhow!(e)),
409 };
410
411 if self.copy_mode == CopyMode::CopyOverwrite || existing_len != Some(len) {
412 generate_delivery_blob(&src, &dst, self.delivery_blob_type).await?;
413 }
414
415 Ok(())
416 }
417 .boxed()
418 }
419
420 fn store_delivery_blob<'a>(
421 &'a self,
422 hash: &Hash,
423 src: &Utf8Path,
424 delivery_blob_type: DeliveryBlobType,
425 ) -> BoxFuture<'a, Result<()>> {
426 let src = src.to_path_buf();
427 let hash = *hash;
428
429 async move {
430 if delivery_blob_type != self.delivery_blob_type {
431 warn!(
432 "storing delivery blob type {:?} in repository with delivery blob type {:?}",
433 delivery_blob_type, self.delivery_blob_type,
434 );
435 }
437 let dst = sanitize_path(
438 &self.blob_repo_path,
439 &format!("{}/{hash}", u32::from(delivery_blob_type)),
440 )?;
441
442 let src_metadata = fs::metadata(&src)?;
443 let dst_metadata = match fs::metadata(&dst) {
444 Ok(metadata) => Some(metadata),
445 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
446 Err(e) => return Err(anyhow!(e)),
447 };
448
449 let dst_is_hardlink = if let Some(dst_metadata) = &dst_metadata {
450 dst_metadata.nlink() > 1
451 } else {
452 false
453 };
454
455 let dst_exists = dst_metadata.is_some();
456 let dst_dirty = !dst_exists;
457
458 match self.copy_mode {
459 CopyMode::Copy => {
460 if dst_dirty || dst_is_hardlink {
461 copy_blob(&src, &dst).await?
462 }
463 }
464 CopyMode::CopyOverwrite => copy_blob(&src, &dst).await?,
465 CopyMode::HardLink => {
466 let is_hardlink = if let Some(dst_metadata) = &dst_metadata {
467 src_metadata.dev() == dst_metadata.dev()
468 && src_metadata.ino() == dst_metadata.ino()
469 } else {
470 false
471 };
472
473 if is_hardlink {
474 } else {
476 if let Some(parent) = dst.parent() {
478 std::fs::create_dir_all(parent)?;
479 }
480 match fs::hard_link(&src, &dst) {
481 Ok(()) => {
482 if fs::metadata(&dst).is_err() {
485 fuchsia_async::Timer::new(std::time::Duration::from_secs(1))
486 .await;
487 if fs::metadata(&dst).is_err() {
488 copy_blob(&src, &dst).await?
489 }
490 }
491 }
492 Err(_) if dst_dirty => copy_blob(&src, &dst).await?,
493 Err(_) => {
494 }
497 }
498 }
499 }
500 }
501 Ok(())
502 }
503 .boxed()
504 }
505}
506
507async fn create_temp_file(path: &Utf8Path) -> Result<TempPath> {
508 let temp_file = if let Some(parent) = path.parent() {
509 DirBuilder::new().recursive(true).create(parent)?;
510
511 NamedTempFile::new_in(parent)?
512 } else {
513 NamedTempFile::new_in(".")?
514 };
515
516 Ok(temp_file.into_temp_path())
517}
518
519async fn set_blob_read_only(path: &Utf8Path) -> Result<()> {
521 let file = fs::File::open(path)?;
522 let mut permissions = file.metadata()?.permissions();
523 permissions.set_readonly(true);
524 file.set_permissions(permissions)?;
525
526 Ok(())
527}
528
529#[cfg(target_os = "linux")]
531async fn reflink(src_path: &Utf8Path, dst_path: &Utf8Path) -> Result<(), std::io::Error> {
532 use std::os::fd::AsRawFd;
533
534 let src = fs::File::open(src_path)?;
535 let dst = fs::File::create(dst_path)?;
536
537 let res = unsafe { libc::ioctl(dst.as_raw_fd(), libc::FICLONE, src.as_raw_fd()) };
539
540 match res {
541 -1 => {
542 let err = std::io::Error::last_os_error();
543
544 drop(dst);
545 let _ = fs::remove_file(dst_path);
546
547 match err.raw_os_error().unwrap() {
548 libc::EOPNOTSUPP |
550 libc::EXDEV |
552 libc::ENOTTY => {
554 Err(std::io::Error::new(std::io::ErrorKind::Unsupported, err))
555 }
556 _ => Err(err),
557 }
558 }
559 _ => Ok(()),
560 }
561}
562
563#[cfg(not(target_os = "linux"))]
564async fn reflink(_src_path: &Utf8Path, _dst_path: &Utf8Path) -> Result<(), std::io::Error> {
565 use libc as _;
566 Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
567}
568
569async fn copy_blob(src: &Utf8Path, dst: &Utf8Path) -> Result<()> {
570 let temp_path = create_temp_file(dst).await?;
571 match reflink(src, (*temp_path).try_into()?).await {
572 Ok(()) => {}
573 Err(e) if e.kind() == std::io::ErrorKind::Unsupported => {
574 let src = src.to_owned();
575 let temp_path = temp_path.to_path_buf();
576 fasync::unblock(move || fs::copy(src, &temp_path)).await?;
577 }
578 Err(e) => return Err(anyhow!(e)),
579 }
580 temp_path.persist(dst)?;
581
582 set_blob_read_only(dst).await
583}
584
585pub(crate) async fn generate_delivery_blob(
586 src: &Utf8Path,
587 dst: &Utf8Path,
588 blob_type: DeliveryBlobType,
589) -> Result<()> {
590 let src_blob = fs::read(src).with_context(|| format!("reading {src}"))?;
591
592 let temp_path = create_temp_file(dst).await?;
593 let file = std::fs::File::create(&temp_path)?;
594 fasync::unblock(move || {
595 delivery_blob::generate_to(blob_type, &src_blob, std::io::BufWriter::new(file))
596 })
597 .await
598 .context("generate delivery blob")?;
599
600 temp_path.persist(dst)?;
601
602 set_blob_read_only(dst).await
603}
604
605#[cfg(not(target_os = "fuchsia"))]
606#[pin_project::pin_project]
607struct WatchStream {
608 _watcher: notify::RecommendedWatcher,
609 #[pin]
610 receiver: futures::channel::mpsc::Receiver<()>,
611}
612
613#[cfg(not(target_os = "fuchsia"))]
614impl Stream for WatchStream {
615 type Item = ();
616 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
617 self.project().receiver.poll_next(cx)
618 }
619}
620
621fn sanitize_path(repo_path: &Utf8Path, resource_path: &str) -> Result<Utf8PathBuf, Error> {
623 let resource_path = Utf8Path::new(resource_path);
624
625 let mut parts = vec![];
626 for component in resource_path.components() {
627 match component {
628 Utf8Component::Normal(part) => {
629 parts.push(part);
630 }
631 _ => {
632 warn!("invalid resource_path: {}", resource_path);
633 return Err(Error::InvalidPath(resource_path.into()));
634 }
635 }
636 }
637
638 let path = parts.into_iter().collect::<Utf8PathBuf>();
639 Ok(repo_path.join(path))
640}
641
642#[derive(Debug, thiserror::Error)]
643#[error(
644 "blob {hash} at {path:?} is {file_size} bytes in size, \
645 but the package manifest indicates it should be {manifest_size} bytes in size"
646)]
647struct BlobSizeMismatchError {
648 hash: Hash,
649 path: Utf8PathBuf,
650 manifest_size: u64,
651 file_size: u64,
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657 use crate::repository::repo_tests::{self, TestEnv as _};
658 use crate::util::CHUNK_SIZE;
659 use assert_matches::assert_matches;
660 use fuchsia_async as fasync;
661 use futures::{FutureExt, StreamExt};
662 use std::fs::File;
663 use std::io::Write as _;
664 use std::time::Duration;
665 struct TestEnv {
666 _tmp: tempfile::TempDir,
667 metadata_path: Utf8PathBuf,
668 blob_path: Utf8PathBuf,
669 repo: FileSystemRepository,
670 }
671
672 impl TestEnv {
673 fn new() -> Self {
674 let tmp = tempfile::tempdir().unwrap();
675 let dir = Utf8Path::from_path(tmp.path()).unwrap();
676 let metadata_path = dir.join("metadata");
677 let blob_path = dir.join("blobs");
678 std::fs::create_dir(&metadata_path).unwrap();
679 std::fs::create_dir(&blob_path).unwrap();
680
681 Self {
682 _tmp: tmp,
683 metadata_path: metadata_path.clone(),
684 blob_path: blob_path.clone(),
685 repo: FileSystemRepository::new(metadata_path, blob_path),
686 }
687 }
688 }
689
690 #[async_trait::async_trait]
691 impl repo_tests::TestEnv for TestEnv {
692 fn supports_range(&self) -> bool {
693 true
694 }
695
696 fn write_metadata(&self, path: &str, bytes: &[u8]) {
697 let file_path = self.metadata_path.join(path);
698 let mut f = File::create(file_path).unwrap();
699 f.write_all(bytes).unwrap();
700 }
701
702 fn write_blob(&self, path: &str, bytes: &[u8]) {
703 let file_path = self.blob_path.join(path);
704 let mut f = File::create(file_path).unwrap();
705 f.write_all(bytes).unwrap();
706 }
707
708 fn repo(&self) -> &dyn RepoProvider {
709 &self.repo
710 }
711 }
712
713 repo_tests::repo_test_suite! {
714 env = TestEnv::new();
715 chunk_size = CHUNK_SIZE;
716 }
717
718 #[fuchsia::test]
719 async fn test_blob_modification_time() {
720 let env = TestEnv::new();
721
722 let f = File::create(env.blob_path.join("empty-blob")).unwrap();
723 let blob_mtime = f.metadata().unwrap().modified().unwrap();
724 drop(f);
725
726 assert_matches!(
727 env.repo.blob_modification_time("empty-blob").await,
728 Ok(Some(t)) if t == blob_mtime
729 );
730 }
731
732 #[fuchsia::test]
733 async fn test_reject_invalid_paths() {
734 let env = TestEnv::new();
735 env.write_metadata("empty", b"");
736
737 assert_matches!(repo_tests::read_metadata(&env, "empty", Range::Full).await, Ok(body) if body == b"");
738 assert_matches!(repo_tests::read_metadata(&env, "subdir/../empty", Range::Full).await,
739 Err(Error::InvalidPath(path)) if path == Utf8Path::new("subdir/../empty")
740 );
741 }
742
743 #[fuchsia::test]
744 async fn test_watch() {
745 let env = TestEnv::new();
746
747 assert!(env.repo.supports_watch());
749
750 let mut watch_stream = env.repo.watch().unwrap().fuse();
751
752 futures::select! {
755 _ = watch_stream.next() => panic!("should not have received an event"),
756 _ = fasync::Timer::new(Duration::from_millis(10)).fuse() => (),
757 };
758
759 env.write_metadata("timestamp.json", br#"{"version":1}"#);
761
762 futures::select! {
763 result = watch_stream.next() => {
764 assert_eq!(result, Some(()));
765 },
766 _ = fasync::Timer::new(Duration::from_secs(10)).fuse() => {
767 panic!("wrote to timestamp.json, but did not get an event");
768 },
769 };
770
771 env.write_metadata("timestamp.json", br#"{"version":2}"#);
773
774 futures::select! {
775 result = watch_stream.next() => {
776 assert_eq!(result, Some(()));
777 },
778 _ = fasync::Timer::new(Duration::from_secs(10)).fuse() => {
779 panic!("wrote to timestamp.json, but did not get an event");
780 },
781 };
782
783 drop(watch_stream);
789 fasync::Timer::new(Duration::from_millis(100)).await;
790 }
791
792 #[fuchsia::test]
793 async fn test_watch_ignores_access() {
794 let env = TestEnv::new();
795
796 env.write_metadata("timestamp.json", br#"{"version":1}"#);
798
799 let mut watch_stream = env.repo.watch().unwrap().fuse();
800
801 let _ = std::fs::read(env.metadata_path.join("timestamp.json")).unwrap();
803
804 futures::select! {
805 _ = watch_stream.next() => {
806 panic!("reading timestamp.json should not trigger a watch event")
807 }
808 _ = fasync::Timer::new(Duration::from_millis(200)).fuse() => (),
809 };
810
811 drop(watch_stream);
812 fasync::Timer::new(Duration::from_millis(100)).await;
813 }
814
815 #[fuchsia::test]
816 async fn test_store_blob_verifies_src_length() {
817 let tmp = tempfile::tempdir().unwrap();
818 let dir = Utf8Path::from_path(tmp.path()).unwrap();
819
820 let metadata_repo_path = dir.join("metadata");
821 let blob_repo_path = dir.join("blobs");
822 std::fs::create_dir(&metadata_repo_path).unwrap();
823 std::fs::create_dir(&blob_repo_path).unwrap();
824
825 let repo = FileSystemRepository::builder(metadata_repo_path, blob_repo_path.clone())
826 .copy_mode(CopyMode::Copy)
827 .build();
828
829 let contents = b"hello world";
831 let path = dir.join("my-blob");
832 std::fs::write(&path, contents).unwrap();
833
834 let hash = fuchsia_merkle::root_from_slice(contents);
835 let err = repo.store_blob(&hash, contents.len() as u64 + 1, &path).await.unwrap_err();
836 assert_matches!(err.downcast_ref::<BlobSizeMismatchError>(), Some(_));
837 }
838
839 #[fuchsia::test]
840 async fn test_store_blob_copy_detects_length_mismatch() {
841 let tmp = tempfile::tempdir().unwrap();
842 let dir = Utf8Path::from_path(tmp.path()).unwrap();
843
844 let metadata_repo_path = dir.join("metadata");
845 let blob_repo_path = dir.join("blobs");
846 std::fs::create_dir(&metadata_repo_path).unwrap();
847 std::fs::create_dir(&blob_repo_path).unwrap();
848
849 let repo = FileSystemRepository::builder(metadata_repo_path, blob_repo_path.clone())
850 .copy_mode(CopyMode::Copy)
851 .build();
852
853 let contents = b"hello world";
855 let hash = fuchsia_merkle::root_from_slice(contents);
856
857 let path = dir.join("my-blob");
858 std::fs::write(&path, contents).unwrap();
859
860 assert_matches!(repo.store_blob(&hash, contents.len() as u64, &path).await, Ok(()));
861
862 let blob_path = blob_repo_path.join(format!("1/{hash}"));
864 let delivery_blob = std::fs::read(&blob_path).unwrap();
865 let actual = delivery_blob::decompress(&delivery_blob).unwrap();
866 assert_eq!(&actual, &contents[..]);
867
868 assert!(std::fs::metadata(&blob_path).unwrap().permissions().readonly());
869
870 let contents2 = b"another hello world";
872 let path2 = dir.join("my-blob2");
873 std::fs::write(&path2, contents2).unwrap();
874 assert_matches!(repo.store_blob(&hash, contents2.len() as u64, &path2).await, Ok(()));
875
876 let delivery_blob = std::fs::read(&blob_path).unwrap();
878 let actual = delivery_blob::decompress(&delivery_blob).unwrap();
879 assert_eq!(&actual, &contents2[..]);
880 }
881
882 #[fuchsia::test]
883 async fn test_store_blob_copy_skips_present_blobs_of_correct_length() {
884 let tmp = tempfile::tempdir().unwrap();
885 let dir = Utf8Path::from_path(tmp.path()).unwrap();
886
887 let metadata_repo_path = dir.join("metadata");
888 let blob_repo_path = dir.join("blobs");
889 std::fs::create_dir(&metadata_repo_path).unwrap();
890 std::fs::create_dir(&blob_repo_path).unwrap();
891
892 let repo = FileSystemRepository::builder(metadata_repo_path, blob_repo_path.clone())
893 .copy_mode(CopyMode::Copy)
894 .build();
895
896 let contents = b"hello world.";
898 let path = dir.join("my-blob");
899 std::fs::write(&path, contents).unwrap();
900
901 let hash = fuchsia_merkle::root_from_slice(contents);
902 assert_matches!(repo.store_blob(&hash, contents.len() as u64, &path).await, Ok(()));
903
904 let blob_path = blob_repo_path.join(format!("1/{hash}"));
906 let delivery_blob = std::fs::read(&blob_path).unwrap();
907 let actual = delivery_blob::decompress(&delivery_blob).unwrap();
908 assert_eq!(&actual, &contents[..]);
909
910 assert!(std::fs::metadata(&blob_path).unwrap().permissions().readonly());
911
912 let contents2 = b"Hello World!";
914 let path2 = dir.join("my-blob2");
915 std::fs::write(&path2, contents2).unwrap();
916 assert_matches!(repo.store_blob(&hash, contents2.len() as u64, &path2).await, Ok(()));
917
918 let delivery_blob = std::fs::read(&blob_path).unwrap();
920 let actual = delivery_blob::decompress(&delivery_blob).unwrap();
921 assert_eq!(&actual, &contents[..]);
922 }
923
924 #[fuchsia::test]
925 async fn test_store_blob_copy_overwrite() {
926 let tmp = tempfile::tempdir().unwrap();
927 let dir = Utf8Path::from_path(tmp.path()).unwrap();
928
929 let metadata_repo_path = dir.join("metadata");
930 let blob_repo_path = dir.join("blobs");
931 std::fs::create_dir(&metadata_repo_path).unwrap();
932 std::fs::create_dir(&blob_repo_path).unwrap();
933
934 let repo = FileSystemRepository::builder(metadata_repo_path, blob_repo_path.clone())
935 .copy_mode(CopyMode::CopyOverwrite)
936 .build();
937
938 let contents = b"hello world";
940 let path = dir.join("my-blob");
941 std::fs::write(&path, contents).unwrap();
942
943 let hash = fuchsia_merkle::root_from_slice(contents);
944 assert_matches!(repo.store_blob(&hash, contents.len() as u64, &path).await, Ok(()));
945
946 let blob_path = blob_repo_path.join(format!("1/{hash}"));
948 let delivery_blob = std::fs::read(&blob_path).unwrap();
949 let actual = delivery_blob::decompress(&delivery_blob).unwrap();
950 assert_eq!(&actual, &contents[..]);
951
952 assert!(std::fs::metadata(&blob_path).unwrap().permissions().readonly());
953
954 let contents2 = b"another blob";
956 let path2 = dir.join("my-blob2");
957 std::fs::write(&path2, contents2).unwrap();
958 assert_matches!(repo.store_blob(&hash, contents2.len() as u64, &path2).await, Ok(()));
959
960 let delivery_blob = std::fs::read(&blob_path).unwrap();
962 let actual = delivery_blob::decompress(&delivery_blob).unwrap();
963 assert_eq!(&actual, &contents2[..]);
964 }
965
966 #[fuchsia::test]
967 async fn test_store_delivery_blob_hard_link() {
968 let tmp = tempfile::tempdir().unwrap();
969 let dir = Utf8Path::from_path(tmp.path()).unwrap();
970
971 let metadata_repo_path = dir.join("metadata");
972 let blob_repo_path = dir.join("blobs");
973 std::fs::create_dir(&metadata_repo_path).unwrap();
974 std::fs::create_dir(&blob_repo_path).unwrap();
975
976 let repo = FileSystemRepository::builder(metadata_repo_path, blob_repo_path.clone())
977 .copy_mode(CopyMode::HardLink)
978 .build();
979
980 let contents = b"hello world";
982 let hash = fuchsia_merkle::root_from_slice(contents);
983
984 let uncompressed_path = dir.join("my-blob");
985 std::fs::write(&uncompressed_path, contents).unwrap();
986 let path = dir.join("my-delivery-blob");
987 generate_delivery_blob(&uncompressed_path, &path, DeliveryBlobType::Type1).await.unwrap();
988
989 assert_matches!(
990 repo.store_delivery_blob(&hash, &path, DeliveryBlobType::Type1).await,
991 Ok(())
992 );
993
994 let blob_path = blob_repo_path.join(format!("1/{hash}"));
996 let delivery_blob = std::fs::read(&blob_path).unwrap();
997 let actual: Vec<u8> = delivery_blob::decompress(&delivery_blob).unwrap();
998 assert_eq!(&actual, &contents[..]);
999
1000 #[cfg(target_family = "unix")]
1001 async fn check_links(blob_path: &Utf8Path) {
1002 use std::os::unix::fs::MetadataExt as _;
1003
1004 assert_eq!(std::fs::metadata(blob_path).unwrap().nlink(), 2);
1005 }
1006
1007 #[cfg(not(target_family = "unix"))]
1008 async fn check_links(_blob_path: &Utf8Path) {}
1009
1010 check_links(&blob_path).await;
1012 }
1013
1014 #[fuchsia::test]
1015 async fn test_store_blob_generates_delivery_blob() {
1016 let tmp = tempfile::tempdir().unwrap();
1017 let dir = Utf8Path::from_path(tmp.path()).unwrap();
1018
1019 let metadata_repo_path = dir.join("metadata");
1020 let blob_repo_path = dir.join("blobs");
1021 std::fs::create_dir(&metadata_repo_path).unwrap();
1022 std::fs::create_dir(&blob_repo_path).unwrap();
1023
1024 let repo = FileSystemRepository::builder(metadata_repo_path, blob_repo_path.clone())
1025 .delivery_blob_type(DeliveryBlobType::Type1)
1026 .build();
1027
1028 let contents = b"hello world";
1030 let hash = fuchsia_merkle::root_from_slice(contents);
1031
1032 let path = dir.join("my-blob");
1033 std::fs::write(&path, contents).unwrap();
1034
1035 assert_matches!(repo.store_blob(&hash, contents.len() as u64, &path).await, Ok(()));
1036
1037 let blob_path = blob_repo_path.join("1").join(hash.to_string());
1039 let delivery_blob = std::fs::read(&blob_path).unwrap();
1040 let actual = delivery_blob::decompress(&delivery_blob).unwrap();
1041 assert_eq!(&actual, &contents[..]);
1042
1043 assert!(std::fs::metadata(&blob_path).unwrap().permissions().readonly());
1044 }
1045
1046 #[fuchsia::test]
1047 async fn test_store_delivery_blob() {
1048 let tmp = tempfile::tempdir().unwrap();
1049 let dir = Utf8Path::from_path(tmp.path()).unwrap();
1050
1051 let metadata_repo_path = dir.join("metadata");
1052 let blob_repo_path = dir.join("blobs");
1053 std::fs::create_dir(&metadata_repo_path).unwrap();
1054 std::fs::create_dir(&blob_repo_path).unwrap();
1055
1056 let repo = FileSystemRepository::builder(metadata_repo_path, blob_repo_path.clone())
1057 .delivery_blob_type(DeliveryBlobType::Type1)
1058 .build();
1059
1060 let contents = b"hello world";
1062 let uncompressed_path = dir.join("my-blob");
1063 std::fs::write(&uncompressed_path, contents).unwrap();
1064 let path = dir.join("my-delivery-blob");
1065 generate_delivery_blob(&uncompressed_path, &path, DeliveryBlobType::Type1).await.unwrap();
1066 let delivery_blob = std::fs::read(&path).unwrap();
1067
1068 let hash = fuchsia_merkle::root_from_slice(contents);
1069 assert_matches!(
1070 repo.store_delivery_blob(&hash, &path, DeliveryBlobType::Type1).await,
1071 Ok(())
1072 );
1073
1074 let blob_path = blob_repo_path.join("1").join(hash.to_string());
1076 let stored_delivery_blob = std::fs::read(&blob_path).unwrap();
1077 assert_eq!(stored_delivery_blob, delivery_blob);
1078
1079 assert!(std::fs::metadata(&blob_path).unwrap().permissions().readonly());
1080 }
1081}