@@ -17,20 +17,141 @@ use super::{abs_view_path, internal, mst2_error_response, request::Mst2Bytes, tr
1717use crate :: ceres:: snapshot:: {
1818 chunks:: { ChunkProjection , get_or_project} ,
1919 error:: { SnapshotError , SnapshotErrorCode } ,
20- pages:: { WalkOutcome , base64_of, hex_of, resolve_abs} ,
20+ pages:: {
21+ MetadataWalkOutcome , WalkOutcome , base64_of, fetch_raw_blob, hex_of, resolve_abs,
22+ resolve_abs_metadata,
23+ } ,
2124 resolver:: FsKind ,
2225 runtime:: runtime,
2326 view:: validate_scope_relative_path,
2427} ;
2528
2629/// One file resolved at a fixed path with verified content.
2730struct ResolvedFile {
28- fs_kind : FsKind ,
2931 digest : [ u8 ; 32 ] ,
3032 size : u64 ,
3133 raw : Vec < u8 > ,
3234}
3335
36+ struct ResolvedFileMetadata {
37+ fs_kind : FsKind ,
38+ oid : String ,
39+ digest : [ u8 ; 32 ] ,
40+ size : u64 ,
41+ }
42+
43+ #[ allow( clippy:: result_large_err) ]
44+ async fn fixed_root_tree < T : crate :: ceres:: api_service:: ApiHandler + ?Sized > (
45+ handler : & T ,
46+ oid : & str ,
47+ ) -> Result < git_internal:: internal:: object:: tree:: Tree , Response > {
48+ let tree = handler. get_tree_by_hash ( oid) . await . map_err ( internal) ?;
49+ if tree. id . to_string ( ) != oid {
50+ return Err ( mst2_error_response ( SnapshotError :: new (
51+ SnapshotErrorCode :: IntegrityError ,
52+ "fetched fixed root tree identity mismatch" ,
53+ ) ) ) ;
54+ }
55+ Ok ( tree)
56+ }
57+
58+ #[ allow( clippy:: result_large_err) ]
59+ async fn resolve_file_metadata < T : crate :: ceres:: api_service:: ApiHandler + ?Sized > (
60+ handler : & T ,
61+ root_tree : & git_internal:: internal:: object:: tree:: Tree ,
62+ scope : & str ,
63+ path : & str ,
64+ expected_digest : Option < & str > ,
65+ ) -> Result < ResolvedFileMetadata , Response > {
66+ validate_scope_relative_path ( path) . map_err ( mst2_error_response) ?;
67+ let ( fs_kind, oid) = match resolve_abs_metadata ( handler, root_tree, & abs_view_path ( scope, path) )
68+ . await
69+ . map_err ( mst2_error_response) ?
70+ {
71+ MetadataWalkOutcome :: FoundFile { fs_kind, oid } => ( fs_kind, oid) ,
72+ MetadataWalkOutcome :: FoundDir => {
73+ return Err ( mst2_error_response ( SnapshotError :: new (
74+ SnapshotErrorCode :: NotDirectory ,
75+ format ! ( "{path} is a directory" ) ,
76+ ) ) ) ;
77+ }
78+ MetadataWalkOutcome :: Absent => {
79+ return Err ( mst2_error_response ( SnapshotError :: new (
80+ SnapshotErrorCode :: PathNotFound ,
81+ format ! ( "{path} absent in the fixed view" ) ,
82+ ) ) ) ;
83+ }
84+ MetadataWalkOutcome :: NotDirectory { symlink } => {
85+ return Err ( mst2_error_response ( SnapshotError :: new (
86+ if symlink {
87+ SnapshotErrorCode :: SymlinkTraversal
88+ } else {
89+ SnapshotErrorCode :: NotDirectory
90+ } ,
91+ format ! ( "{path}: intermediate component is not a directory" ) ,
92+ ) ) ) ;
93+ }
94+ } ;
95+ let storage = handler. get_context ( ) ;
96+ let mut verified = storage
97+ . mono_storage ( )
98+ . get_verified_blobs ( vec ! [ oid. clone( ) ] )
99+ . await
100+ . map_err ( |error| {
101+ let code = if matches ! (
102+ error,
103+ crate :: common:: errors:: MegaError :: ObjStorageInconsistent ( _)
104+ ) {
105+ SnapshotErrorCode :: IntegrityError
106+ } else {
107+ SnapshotErrorCode :: Internal
108+ } ;
109+ tracing:: warn!( error = %error, "fixed content metadata lookup failed" ) ;
110+ mst2_error_response ( SnapshotError :: new (
111+ code,
112+ "fixed content metadata lookup failed" ,
113+ ) )
114+ } ) ?;
115+ let fact = verified. remove ( & oid) . ok_or_else ( || {
116+ mst2_error_response ( SnapshotError :: new (
117+ SnapshotErrorCode :: MetadataNotReady ,
118+ "fixed object has no current verified size and digest fact" ,
119+ ) )
120+ } ) ?;
121+ let digest: [ u8 ; 32 ] = fact. raw_sha256 . as_slice ( ) . try_into ( ) . map_err ( |_| {
122+ mst2_error_response ( SnapshotError :: new (
123+ SnapshotErrorCode :: IntegrityError ,
124+ "invalid verified content digest" ,
125+ ) )
126+ } ) ?;
127+ let size = u64:: try_from ( fact. size ) . map_err ( |_| {
128+ mst2_error_response ( SnapshotError :: new (
129+ SnapshotErrorCode :: IntegrityError ,
130+ "invalid verified content size" ,
131+ ) )
132+ } ) ?;
133+ if fs_kind == FsKind :: Symlink && !( 1 ..=4095 ) . contains ( & size) {
134+ return Err ( mst2_error_response ( SnapshotError :: new (
135+ SnapshotErrorCode :: IntegrityError ,
136+ "verified symlink size is outside the fixed filesystem profile" ,
137+ ) ) ) ;
138+ }
139+ if let Some ( expected) = expected_digest
140+ && expected != format ! ( "sha256:{}" , hex_of( & digest) )
141+ {
142+ return Err ( mst2_error_response ( SnapshotError :: new (
143+ SnapshotErrorCode :: DigestMismatch ,
144+ format ! ( "{path}: content does not match expected_digest" ) ,
145+ ) ) ) ;
146+ }
147+ Ok ( ResolvedFileMetadata {
148+ fs_kind,
149+ oid,
150+ digest,
151+ size,
152+ } )
153+ }
154+
34155pub ( super ) fn fs_kind_str ( k : FsKind ) -> & ' static str {
35156 match k {
36157 FsKind :: Regular => "regular" ,
@@ -57,11 +178,7 @@ async fn resolve_file<T: crate::ceres::api_service::ApiHandler + ?Sized>(
57178 . map_err ( mst2_error_response) ?
58179 {
59180 WalkOutcome :: FoundFile {
60- fs_kind,
61- raw,
62- size,
63- digest,
64- ..
181+ raw, size, digest, ..
65182 } => {
66183 if let Some ( expected) = expected_digest
67184 && expected != format ! ( "sha256:{}" , hex_of( & digest) )
@@ -71,12 +188,7 @@ async fn resolve_file<T: crate::ceres::api_service::ApiHandler + ?Sized>(
71188 format ! ( "{path}: content does not match expected_digest" ) ,
72189 ) ) ) ;
73190 }
74- Ok ( ResolvedFile {
75- fs_kind,
76- digest,
77- size,
78- raw,
79- } )
191+ Ok ( ResolvedFile { digest, size, raw } )
80192 }
81193 WalkOutcome :: FoundDir => Err ( mst2_error_response ( SnapshotError :: new (
82194 SnapshotErrorCode :: NotDirectory ,
@@ -128,11 +240,8 @@ pub(super) async fn blob_head(
128240 . api_handler ( std:: path:: Path :: new ( "/" ) )
129241 . await
130242 . map_err ( internal) ?;
131- let root_tree = handler
132- . get_tree_by_hash ( & ctx. root_tree_oid )
133- . await
134- . map_err ( internal) ?;
135- let f = resolve_file (
243+ let root_tree = fixed_root_tree ( handler. as_ref ( ) , & ctx. root_tree_oid ) . await ?;
244+ let f = resolve_file_metadata (
136245 handler. as_ref ( ) ,
137246 & root_tree,
138247 & ctx. built . descriptor . scope ,
@@ -144,6 +253,7 @@ pub(super) async fn blob_head(
144253 . header ( "content-length" , f. size . to_string ( ) )
145254 . header ( "etag" , format ! ( "\" sha256:{}\" " , hex_of( & f. digest) ) )
146255 . header ( "cache-control" , "private, no-cache, no-transform" )
256+ . header ( "vary" , "Authorization, Accept" )
147257 . header ( "x-mega-fs-kind" , fs_kind_str ( f. fs_kind ) )
148258 . header ( "x-mega-content-size" , f. size . to_string ( ) )
149259 . body ( axum:: body:: Body :: empty ( ) )
@@ -201,10 +311,7 @@ pub(super) async fn objects(
201311 . api_handler ( std:: path:: Path :: new ( "/" ) )
202312 . await
203313 . map_err ( internal) ?;
204- let root_tree = handler
205- . get_tree_by_hash ( & ctx. root_tree_oid )
206- . await
207- . map_err ( internal) ?;
314+ let root_tree = fixed_root_tree ( handler. as_ref ( ) , & ctx. root_tree_oid ) . await ?;
208315 let scope = ctx. built . descriptor . scope . clone ( ) ;
209316 // Copied references: each per-item future borrows the shared walk state
210317 // without moving it (the stream is an FnMut over owned items).
@@ -317,23 +424,40 @@ async fn project_for<T: crate::ceres::api_service::ApiHandler + ?Sized>(
317424 path : & str ,
318425 expected_digest : Option < & str > ,
319426) -> Result < std:: sync:: Arc < ChunkProjection > , Response > {
320- let f = resolve_file ( handler, root_tree, scope, path, expected_digest) . await ?;
427+ let f = resolve_file_metadata ( handler, root_tree, scope, path, expected_digest) . await ?;
321428 // The first request for a digest builds the projection from the fixed
322429 // Git object; later requests slice the cached representation. A miss
323430 // rebuilds, never errors with "missing chunk".
324431 let digest = f. digest ;
325- get_or_project ( digest, || async move {
326- let raw = f. raw ;
327- if raw. len ( ) as u64 != f. size {
432+ let size = f. size ;
433+ let projection = get_or_project ( digest, || async move {
434+ let raw = fetch_raw_blob ( handler, & f. oid ) . await ?;
435+ if raw. len ( ) as u64 != size {
328436 return Err ( SnapshotError :: new (
329- SnapshotErrorCode :: Internal ,
330- "resolved blob size disagrees with its length " ,
437+ SnapshotErrorCode :: IntegrityError ,
438+ "fixed blob length disagrees with its verified size fact " ,
331439 ) ) ;
332440 }
333441 Ok ( raw)
334442 } )
335443 . await
336- . map_err ( mst2_error_response)
444+ . map_err ( |error| {
445+ mst2_error_response ( if error. code == SnapshotErrorCode :: DigestMismatch {
446+ SnapshotError :: new (
447+ SnapshotErrorCode :: IntegrityError ,
448+ "fixed blob digest disagrees with its verified fact" ,
449+ )
450+ } else {
451+ error
452+ } )
453+ } ) ?;
454+ if projection. map . file_size != size || projection. map . file_content_id != digest {
455+ return Err ( mst2_error_response ( SnapshotError :: new (
456+ SnapshotErrorCode :: IntegrityError ,
457+ "cached projection disagrees with the fixed verified fact" ,
458+ ) ) ) ;
459+ }
460+ Ok ( projection)
337461}
338462
339463#[ allow( clippy:: result_large_err) ]
@@ -351,10 +475,7 @@ pub(super) async fn chunk_map(
351475 . api_handler ( std:: path:: Path :: new ( "/" ) )
352476 . await
353477 . map_err ( internal) ?;
354- let root_tree = handler
355- . get_tree_by_hash ( & ctx. root_tree_oid )
356- . await
357- . map_err ( internal) ?;
478+ let root_tree = fixed_root_tree ( handler. as_ref ( ) , & ctx. root_tree_oid ) . await ?;
358479 let scope = ctx. built . descriptor . scope . clone ( ) ;
359480 let proj = project_for (
360481 handler. as_ref ( ) ,
@@ -419,10 +540,7 @@ pub(super) async fn chunk_map_pages(
419540 . api_handler ( std:: path:: Path :: new ( "/" ) )
420541 . await
421542 . map_err ( internal) ?;
422- let root_tree = handler
423- . get_tree_by_hash ( & ctx. root_tree_oid )
424- . await
425- . map_err ( internal) ?;
543+ let root_tree = fixed_root_tree ( handler. as_ref ( ) , & ctx. root_tree_oid ) . await ?;
426544 let scope = ctx. built . descriptor . scope . clone ( ) ;
427545 let proj = project_for (
428546 handler. as_ref ( ) ,
@@ -528,10 +646,7 @@ pub(super) async fn chunks(
528646 . api_handler ( std:: path:: Path :: new ( "/" ) )
529647 . await
530648 . map_err ( internal) ?;
531- let root_tree = handler
532- . get_tree_by_hash ( & ctx. root_tree_oid )
533- . await
534- . map_err ( internal) ?;
649+ let root_tree = fixed_root_tree ( handler. as_ref ( ) , & ctx. root_tree_oid ) . await ?;
535650 let scope = ctx. built . descriptor . scope . clone ( ) ;
536651 let mut planned: Vec < Planned > = Vec :: new ( ) ;
537652 let mut units: Vec < ( String , u64 ) > = Vec :: new ( ) ;
@@ -659,3 +774,7 @@ fn ensure(state: &crate::api::MonoApiServiceState) -> Result<(), Response> {
659774 ) ) )
660775 }
661776}
777+
778+ #[ cfg( test) ]
779+ #[ path = "snapshot_content_tests.rs" ]
780+ mod tests;
0 commit comments