@@ -451,10 +451,13 @@ impl QcowHeader {
451451 Ok ( QcowHeader {
452452 magic : QCOW_MAGIC ,
453453 version,
454- backing_file_offset : ( if backing_file. is_none ( ) {
455- 0
456- } else {
454+ backing_file_offset : backing_file. map_or ( 0 , |_| {
457455 header_size
456+ + if version == 3 {
457+ QCOW_EMPTY_HEADER_EXTENSION_SIZE
458+ } else {
459+ 0
460+ }
458461 } ) as u64 ,
459462 backing_file_size : backing_file. map_or ( 0 , |x| x. len ( ) ) as u32 ,
460463 cluster_bits : DEFAULT_CLUSTER_BITS ,
@@ -528,11 +531,20 @@ impl QcowHeader {
528531 write_u64_to_file ( file, self . autoclear_features ) ?;
529532 write_u32_to_file ( file, self . refcount_order ) ?;
530533 write_u32_to_file ( file, self . header_size ) ?;
534+
535+ if self . header_size > V3_BARE_HEADER_SIZE {
536+ write_u64_to_file ( file, 0 ) ?; // no compression
537+ }
538+
531539 write_u32_to_file ( file, 0 ) ?; // header extension type: end of header extension area
532540 write_u32_to_file ( file, 0 ) ?; // length of header extension data: 0
533541 }
534542
535543 if let Some ( backing_file_path) = self . backing_file . as_ref ( ) . map ( |bf| & bf. path ) {
544+ if self . backing_file_offset > 0 {
545+ file. seek ( SeekFrom :: Start ( self . backing_file_offset ) )
546+ . map_err ( Error :: WritingHeader ) ?;
547+ }
536548 write ! ( file, "{backing_file_path}" ) . map_err ( Error :: WritingHeader ) ?;
537549 }
538550
@@ -2324,6 +2336,120 @@ mod unit_tests {
23242336 ) ;
23252337 }
23262338
2339+ /// Helper to create a test file with header extensions
2340+ fn create_header_with_extension ( ext_type : u32 , ext_data : & [ u8 ] ) -> ( RawFile , QcowHeader ) {
2341+ let header = QcowHeader :: create_for_size_and_path ( 3 , 0x10_0000 , None )
2342+ . expect ( "Failed to create header." ) ;
2343+
2344+ let mut disk_file: RawFile = RawFile :: new ( TempFile :: new ( ) . unwrap ( ) . into_file ( ) , false ) ;
2345+ header. write_to ( & mut disk_file) . unwrap ( ) ;
2346+
2347+ // Write extension
2348+ disk_file
2349+ . seek ( SeekFrom :: Start ( header. header_size as u64 ) )
2350+ . unwrap ( ) ;
2351+ disk_file. write_u32 :: < BigEndian > ( ext_type) . unwrap ( ) ;
2352+ disk_file
2353+ . write_u32 :: < BigEndian > ( ext_data. len ( ) as u32 )
2354+ . unwrap ( ) ;
2355+ disk_file. write_all ( ext_data) . unwrap ( ) ;
2356+
2357+ // Add padding to 8-byte boundary
2358+ let padding = ( 8 - ( ext_data. len ( ) % 8 ) ) % 8 ;
2359+ if padding > 0 {
2360+ disk_file. write_all ( & vec ! [ 0u8 ; padding] ) . unwrap ( ) ;
2361+ }
2362+
2363+ disk_file. write_u32 :: < BigEndian > ( HEADER_EXT_END ) . unwrap ( ) ;
2364+
2365+ disk_file. rewind ( ) . unwrap ( ) ;
2366+
2367+ ( disk_file, header)
2368+ }
2369+
2370+ #[ test]
2371+ fn read_header_extensions_unknown_extension ( ) {
2372+ let ( mut disk_file, mut header) = create_header_with_extension (
2373+ 0x12345678 , // unknown type
2374+ "test" . as_bytes ( ) ,
2375+ ) ;
2376+
2377+ // Extension parsing needs a backing file to set format on
2378+ header. backing_file = Some ( BackingFileConfig {
2379+ path : "/test/backing" . to_string ( ) ,
2380+ format : None ,
2381+ } ) ;
2382+
2383+ QcowHeader :: read_header_extensions ( & mut disk_file, & mut header) . unwrap ( ) ;
2384+ assert_eq ! ( header. backing_file. as_ref( ) . and_then( |bf| bf. format) , None ) ;
2385+ }
2386+
2387+ #[ test]
2388+ fn read_header_extensions_raw_format ( ) {
2389+ let ( mut disk_file, mut header) =
2390+ create_header_with_extension ( HEADER_EXT_BACKING_FORMAT , "raw" . as_bytes ( ) ) ;
2391+
2392+ header. backing_file = Some ( BackingFileConfig {
2393+ path : "/test/backing" . to_string ( ) ,
2394+ format : None ,
2395+ } ) ;
2396+
2397+ QcowHeader :: read_header_extensions ( & mut disk_file, & mut header) . unwrap ( ) ;
2398+ assert_eq ! (
2399+ header. backing_file. as_ref( ) . and_then( |bf| bf. format) ,
2400+ Some ( ImageType :: Raw )
2401+ ) ;
2402+ }
2403+
2404+ #[ test]
2405+ fn read_header_extensions_qcow2_format ( ) {
2406+ let ( mut disk_file, mut header) =
2407+ create_header_with_extension ( HEADER_EXT_BACKING_FORMAT , "qcow2" . as_bytes ( ) ) ;
2408+
2409+ header. backing_file = Some ( BackingFileConfig {
2410+ path : "/test/backing" . to_string ( ) ,
2411+ format : None ,
2412+ } ) ;
2413+
2414+ QcowHeader :: read_header_extensions ( & mut disk_file, & mut header) . unwrap ( ) ;
2415+ assert_eq ! (
2416+ header. backing_file. as_ref( ) . and_then( |bf| bf. format) ,
2417+ Some ( ImageType :: Qcow2 )
2418+ ) ;
2419+ }
2420+
2421+ #[ test]
2422+ fn read_header_extensions_invalid_format ( ) {
2423+ let ( mut disk_file, mut header) =
2424+ create_header_with_extension ( HEADER_EXT_BACKING_FORMAT , "vmdk" . as_bytes ( ) ) ;
2425+
2426+ header. backing_file = Some ( BackingFileConfig {
2427+ path : "/test/backing" . to_string ( ) ,
2428+ format : None ,
2429+ } ) ;
2430+
2431+ let result = QcowHeader :: read_header_extensions ( & mut disk_file, & mut header) ;
2432+ assert ! ( matches!(
2433+ result. unwrap_err( ) ,
2434+ Error :: UnsupportedBackingFileFormat ( _)
2435+ ) ) ;
2436+ }
2437+
2438+ #[ test]
2439+ fn read_header_extensions_invalid_utf8 ( ) {
2440+ let ( mut disk_file, mut header) = create_header_with_extension (
2441+ HEADER_EXT_BACKING_FORMAT ,
2442+ & [ 0xFF , 0xFE , 0xFD ] , // invalid UTF-8
2443+ ) ;
2444+
2445+ let result = QcowHeader :: read_header_extensions ( & mut disk_file, & mut header) ;
2446+ // Should fail with InvalidBackingFileName error
2447+ assert ! ( matches!(
2448+ result. unwrap_err( ) ,
2449+ Error :: InvalidBackingFileName ( _)
2450+ ) ) ;
2451+ }
2452+
23272453 #[ test]
23282454 fn no_backing_file ( ) {
23292455 // `backing_file` is `None`
0 commit comments