88use std:: sync:: Arc ;
99
1010use hyperlight_testing:: { c_simple_guest_as_pathbuf, simple_guest_as_pathbuf} ;
11+ use serde:: { Deserialize , Serialize } ;
1112use serde_json:: Value ;
1213use sha2:: { Digest as _, Sha256 } ;
1314
@@ -51,6 +52,12 @@ fn create_snapshot() -> Arc<Snapshot> {
5152 sbox. snapshot ( ) . unwrap ( )
5253}
5354
55+ #[ derive( Debug , PartialEq , Eq , Serialize , Deserialize ) ]
56+ struct TestMetadata {
57+ format_version : u32 ,
58+ runtime : String ,
59+ }
60+
5461/// `Result::unwrap_err` requires `T: Debug`, but `Snapshot` is not
5562/// `Debug`. This wrapper is the test-side equivalent.
5663#[ track_caller]
@@ -144,6 +151,45 @@ fn from_snapshot_in_memory_pre_init() {
144151 assert_eq ! ( result, 0 ) ;
145152}
146153
154+ #[ test]
155+ fn snapshot_metadata_is_immutable_and_shares_state ( ) {
156+ let snapshot = create_snapshot ( ) ;
157+ let metadata = TestMetadata {
158+ format_version : 1 ,
159+ runtime : "test" . to_string ( ) ,
160+ } ;
161+
162+ let with_metadata = snapshot
163+ . with_metadata ( "snapshot-metadata-namespace-v1" , & metadata)
164+ . unwrap ( ) ;
165+
166+ assert ! ( Arc :: ptr_eq( & snapshot. state, & with_metadata. state) ) ;
167+ assert_eq ! (
168+ snapshot
169+ . metadata:: <TestMetadata >( "snapshot-metadata-namespace-v1" )
170+ . unwrap( ) ,
171+ None
172+ ) ;
173+ assert_eq ! (
174+ with_metadata
175+ . metadata:: <TestMetadata >( "snapshot-metadata-namespace-v1" )
176+ . unwrap( ) ,
177+ Some ( metadata)
178+ ) ;
179+ }
180+
181+ #[ test]
182+ fn snapshot_metadata_namespaces_are_independent ( ) {
183+ let snapshot = create_snapshot ( ) ;
184+ let first = snapshot. with_metadata ( "first" , & 1_u32 ) . unwrap ( ) ;
185+ let second = first. with_metadata ( "second" , & 2_u32 ) . unwrap ( ) ;
186+ let replaced = second. with_metadata ( "first" , & 3_u32 ) . unwrap ( ) ;
187+
188+ assert_eq ! ( second. metadata:: <u32 >( "first" ) . unwrap( ) , Some ( 1 ) ) ;
189+ assert_eq ! ( replaced. metadata:: <u32 >( "first" ) . unwrap( ) , Some ( 3 ) ) ;
190+ assert_eq ! ( replaced. metadata:: <u32 >( "second" ) . unwrap( ) , Some ( 2 ) ) ;
191+ }
192+
147193// Round-trip via OCI layout on disk.
148194
149195#[ test]
@@ -164,6 +210,59 @@ fn round_trip_save_load_call() {
164210 assert_eq ! ( result, "hello\n " ) ;
165211}
166212
213+ #[ test]
214+ fn snapshot_metadata_round_trip ( ) {
215+ let metadata = TestMetadata {
216+ format_version : 1 ,
217+ runtime : "test" . to_string ( ) ,
218+ } ;
219+ let snapshot = create_snapshot ( )
220+ . with_metadata ( "snapshot-metadata-namespace-v1" , & metadata)
221+ . unwrap ( ) ;
222+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
223+ let path = dir. path ( ) . join ( "metadata" ) ;
224+
225+ snapshot
226+ . save ( & path, & OciTag :: new ( "latest" ) . unwrap ( ) )
227+ . unwrap ( ) ;
228+ let config: Value =
229+ serde_json:: from_slice ( & std:: fs:: read ( find_config_blob ( & path) ) . unwrap ( ) ) . unwrap ( ) ;
230+ assert_eq ! (
231+ config[ "metadata" ] ,
232+ serde_json:: json!( {
233+ "snapshot-metadata-namespace-v1" : {
234+ "format_version" : 1 ,
235+ "runtime" : "test" ,
236+ }
237+ } )
238+ ) ;
239+ let loaded = Snapshot :: checked_load ( & path, OciTag :: new ( "latest" ) . unwrap ( ) ) . unwrap ( ) ;
240+
241+ assert_eq ! (
242+ loaded
243+ . metadata:: <TestMetadata >( "snapshot-metadata-namespace-v1" )
244+ . unwrap( ) ,
245+ Some ( metadata)
246+ ) ;
247+ }
248+
249+ #[ test]
250+ fn snapshot_without_metadata_omits_config_field ( ) {
251+ let snapshot = create_snapshot ( ) ;
252+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
253+ let path = dir. path ( ) . join ( "metadata" ) ;
254+
255+ snapshot
256+ . save ( & path, & OciTag :: new ( "latest" ) . unwrap ( ) )
257+ . unwrap ( ) ;
258+ let config: Value =
259+ serde_json:: from_slice ( & std:: fs:: read ( find_config_blob ( & path) ) . unwrap ( ) ) . unwrap ( ) ;
260+ let loaded = Snapshot :: checked_load ( & path, OciTag :: new ( "latest" ) . unwrap ( ) ) . unwrap ( ) ;
261+
262+ assert ! ( config. get( "metadata" ) . is_none( ) ) ;
263+ assert_eq ! ( loaded. metadata:: <Value >( "missing" ) . unwrap( ) , None ) ;
264+ }
265+
167266/// A pre-existing snapshot blob with the right length but wrong
168267/// bytes (corruption, partial copy, foreign tool) must be detected
169268/// and replaced by `save`, not silently trusted.
0 commit comments