66
77use cap_std_ext:: prelude:: CapStdExtCommandExt ;
88use cap_std_ext:: { cap_std, cap_tempfile} ;
9- use futures_util:: Future ;
9+ use futures_util:: { Future , FutureExt } ;
1010use oci_spec:: image:: { Descriptor , Digest } ;
1111use serde:: { Deserialize , Serialize } ;
1212use std:: fs:: File ;
13+ use std:: num:: NonZeroU32 ;
1314use std:: ops:: Range ;
1415use std:: os:: fd:: OwnedFd ;
1516use std:: os:: unix:: prelude:: CommandExt ;
@@ -64,6 +65,18 @@ impl Error {
6465 }
6566}
6667
68+ /// Errors returned by get_raw_blob
69+ #[ derive( Error , Debug ) ]
70+ #[ non_exhaustive]
71+ pub enum GetBlobError {
72+ /// A client may reasonably retry on this type of error.
73+ #[ error( "retryable error" ) ]
74+ Retryable ( Box < str > ) ,
75+ #[ error( "error" ) ]
76+ /// An unknown other error
77+ Other ( Box < str > ) ,
78+ }
79+
6780impl From < rustix:: io:: Errno > for Error {
6881 fn from ( value : rustix:: io:: Errno ) -> Self {
6982 Self :: Io ( value. into ( ) )
@@ -318,6 +331,12 @@ pub struct ConvertedLayerInfo {
318331 pub media_type : oci_spec:: image:: MediaType ,
319332}
320333
334+ /// Maps the two types of return values from the proxy
335+ enum FileDescriptors {
336+ FinishPipe { pipeid : NonZeroU32 , datafd : OwnedFd } ,
337+ DualFds { datafd : OwnedFd , errfd : OwnedFd } ,
338+ }
339+
321340impl ImageProxy {
322341 /// Create an image proxy that fetches the target image, using default configuration.
323342 pub async fn new ( ) -> Result < Self > {
@@ -373,7 +392,7 @@ impl ImageProxy {
373392 async fn impl_request_raw < T : serde:: de:: DeserializeOwned + Send + ' static > (
374393 sockfd : Arc < Mutex < OwnedFd > > ,
375394 req : Request ,
376- ) -> Result < ( T , Option < ( OwnedFd , u32 ) > ) > {
395+ ) -> Result < ( T , Option < FileDescriptors > ) > {
377396 tracing:: trace!( "sending request {}" , req. method. as_str( ) ) ;
378397 // TODO: Investigate https://crates.io/crates/uds for SOCK_SEQPACKET tokio
379398 let r = tokio:: task:: spawn_blocking ( move || {
@@ -394,14 +413,14 @@ impl ImageProxy {
394413 rustix:: net:: RecvFlags :: CMSG_CLOEXEC ,
395414 ) ?
396415 . bytes ;
397- let fdret = cmsg_buffer
416+ let mut fdret = cmsg_buffer
398417 . drain ( )
399418 . filter_map ( |m| match m {
400419 rustix:: net:: RecvAncillaryMessage :: ScmRights ( f) => Some ( f) ,
401420 _ => None ,
402421 } )
403422 . flatten ( )
404- . next ( ) ;
423+ . fuse ( ) ;
405424 let buf = & buf[ ..nread] ;
406425 let reply: Reply = serde_json:: from_slice ( buf) ?;
407426 if !reply. success {
@@ -410,21 +429,42 @@ impl ImageProxy {
410429 error : reply. error . into ( ) ,
411430 } ) ;
412431 }
413- let fdret = match ( fdret, reply. pipeid ) {
414- ( Some ( fd) , n) => {
415- if n == 0 {
416- return Err ( Error :: Other ( "got fd but no pipeid" . into ( ) ) ) ;
417- }
418- Some ( ( fd, n) )
432+ let first_fd = fdret. next ( ) ;
433+ let second_fd = fdret. next ( ) ;
434+ if fdret. next ( ) . is_some ( ) {
435+ return Err ( Error :: Other (
436+ format ! ( "got more than two file descriptors" ) . into ( ) ,
437+ ) ) ;
438+ }
439+ let pipeid = NonZeroU32 :: new ( reply. pipeid ) ;
440+ let fdret = match ( first_fd, second_fd, pipeid) {
441+ // No fds, no pipeid
442+ ( None , None , None ) => None ,
443+ // A FinishPipe instance
444+ ( Some ( datafd) , None , Some ( pipeid) ) => {
445+ Some ( FileDescriptors :: FinishPipe { pipeid, datafd } )
446+ }
447+ // A dualfd instance
448+ ( Some ( datafd) , Some ( errfd) , None ) => {
449+ Some ( FileDescriptors :: DualFds { datafd, errfd } )
450+ }
451+ // Everything after here is error cases
452+ ( Some ( _) , None , None ) => {
453+ return Err ( Error :: Other ( format ! ( "got fd with zero pipeid" ) . into ( ) ) ) ;
454+ }
455+ ( None , Some ( _) , _) => {
456+ return Err ( Error :: Other ( format ! ( "got errfd with no datafd" ) . into ( ) ) ) ;
457+ }
458+ ( Some ( _) , Some ( _) , Some ( n) ) => {
459+ return Err ( Error :: Other (
460+ format ! ( "got pipeid {} with both datafd and errfd" , n) . into ( ) ,
461+ ) ) ;
419462 }
420- ( None , n) => {
421- if n != 0 {
422- return Err ( Error :: Other ( format ! ( "got no fd with pipeid {}" , n) . into ( ) ) ) ;
423- }
424- None
463+ ( None , _, Some ( n) ) => {
464+ return Err ( Error :: Other ( format ! ( "got no fd with pipeid {n}" ) . into ( ) ) ) ;
425465 }
426466 } ;
427- let reply = serde_json:: from_value ( reply. value ) ?;
467+ let reply: T = serde_json:: from_value ( reply. value ) ?;
428468 Ok ( ( reply, fdret) )
429469 } )
430470 . await
@@ -438,7 +478,7 @@ impl ImageProxy {
438478 & self ,
439479 method : & str ,
440480 args : T ,
441- ) -> Result < ( R , Option < ( OwnedFd , u32 ) > ) >
481+ ) -> Result < ( R , Option < FileDescriptors > ) >
442482 where
443483 T : IntoIterator < Item = I > ,
444484 I : Into < serde_json:: Value > ,
@@ -456,9 +496,9 @@ impl ImageProxy {
456496 }
457497
458498 #[ instrument]
459- async fn finish_pipe ( & self , pipeid : u32 ) -> Result < ( ) > {
499+ async fn finish_pipe ( & self , pipeid : NonZeroU32 ) -> Result < ( ) > {
460500 tracing:: debug!( "closing pipe" ) ;
461- let ( r, fd) = self . impl_request ( "FinishPipe" , [ pipeid] ) . await ?;
501+ let ( r, fd) = self . impl_request ( "FinishPipe" , [ pipeid. get ( ) ] ) . await ?;
462502 if fd. is_some ( ) {
463503 return Err ( Error :: Other ( "Unexpected fd in finish_pipe reply" . into ( ) ) ) ;
464504 }
@@ -494,9 +534,12 @@ impl ImageProxy {
494534 Ok ( r)
495535 }
496536
497- async fn read_all_fd ( & self , fd : Option < ( OwnedFd , u32 ) > ) -> Result < Vec < u8 > > {
498- let ( fd, pipeid) = fd. ok_or_else ( || Error :: Other ( "Missing fd from reply" . into ( ) ) ) ?;
499- let fd = tokio:: fs:: File :: from_std ( std:: fs:: File :: from ( fd) ) ;
537+ async fn read_all_fd ( & self , fd : Option < FileDescriptors > ) -> Result < Vec < u8 > > {
538+ let fd = fd. ok_or_else ( || Error :: Other ( "Missing fd from reply" . into ( ) ) ) ?;
539+ let FileDescriptors :: FinishPipe { pipeid, datafd } = fd else {
540+ return Err ( Error :: Other ( "got dualfds, expecting FinishPipe fd" . into ( ) ) ) ;
541+ } ;
542+ let fd = tokio:: fs:: File :: from_std ( std:: fs:: File :: from ( datafd) ) ;
500543 let mut fd = tokio:: io:: BufReader :: new ( fd) ;
501544 let mut r = Vec :: new ( ) ;
502545 let reader = fd. read_to_end ( & mut r) ;
@@ -569,13 +612,67 @@ impl ImageProxy {
569612 let args: Vec < serde_json:: Value > =
570613 vec ! [ img. 0 . into( ) , digest. to_string( ) . into( ) , size. into( ) ] ;
571614 let ( _bloblen, fd) = self . impl_request :: < i64 , _ , _ > ( "GetBlob" , args) . await ?;
572- let ( fd, pipeid) = fd. ok_or_else ( || Error :: new_other ( "Missing fd from reply" ) ) ?;
573- let fd = tokio:: fs:: File :: from_std ( std:: fs:: File :: from ( fd) ) ;
615+ let fd = fd. ok_or_else ( || Error :: Other ( "Missing fd from reply" . into ( ) ) ) ?;
616+ let FileDescriptors :: FinishPipe { pipeid, datafd } = fd else {
617+ return Err ( Error :: Other ( "got dualfds, expecting FinishPipe fd" . into ( ) ) ) ;
618+ } ;
619+ let fd = tokio:: fs:: File :: from_std ( std:: fs:: File :: from ( datafd) ) ;
574620 let fd = tokio:: io:: BufReader :: new ( fd) ;
575621 let finish = Box :: pin ( self . finish_pipe ( pipeid) ) ;
576622 Ok ( ( fd, finish) )
577623 }
578624
625+ async fn read_blob_error ( fd : OwnedFd ) -> std:: result:: Result < ( ) , GetBlobError > {
626+ let fd = tokio:: fs:: File :: from_std ( std:: fs:: File :: from ( fd) ) ;
627+ let mut errfd = tokio:: io:: BufReader :: new ( fd) ;
628+ let mut buf = Vec :: new ( ) ;
629+ errfd
630+ . read_to_end ( & mut buf)
631+ . await
632+ . map_err ( |e| GetBlobError :: Other ( e. to_string ( ) . into_boxed_str ( ) ) ) ?;
633+ if buf. is_empty ( ) {
634+ return Ok ( ( ) ) ;
635+ }
636+ #[ derive( Deserialize ) ]
637+ struct RemoteError {
638+ code : String ,
639+ message : String ,
640+ }
641+ let e: RemoteError = serde_json:: from_slice ( & buf)
642+ . map_err ( |e| GetBlobError :: Other ( e. to_string ( ) . into_boxed_str ( ) ) ) ?;
643+ match e. code . as_str ( ) {
644+ // Actually this is OK
645+ "EPIPE" => Ok ( ( ) ) ,
646+ "retryable" => Err ( GetBlobError :: Retryable ( e. message . into_boxed_str ( ) ) ) ,
647+ _ => Err ( GetBlobError :: Other ( e. message . into_boxed_str ( ) ) ) ,
648+ }
649+ }
650+
651+ /// Fetch a blob identified by e.g. `sha256:<digest>`; does not perform
652+ /// any verification that the blob matches the digest. The size of the
653+ /// blob and a pipe file descriptor are returned.
654+ #[ instrument]
655+ pub async fn get_raw_blob (
656+ & self ,
657+ img : & OpenedImage ,
658+ digest : & Digest ,
659+ ) -> Result < (
660+ u64 ,
661+ tokio:: fs:: File ,
662+ impl Future < Output = std:: result:: Result < ( ) , GetBlobError > > + Unpin + ' _ ,
663+ ) > {
664+ tracing:: debug!( "fetching blob" ) ;
665+ let args: Vec < serde_json:: Value > = vec ! [ img. 0 . into( ) , digest. to_string( ) . into( ) ] ;
666+ let ( bloblen, fd) = self . impl_request :: < u64 , _ , _ > ( "GetRawBlob" , args) . await ?;
667+ let fd = fd. ok_or_else ( || Error :: new_other ( "Missing fd from reply" ) ) ?;
668+ let FileDescriptors :: DualFds { datafd, errfd } = fd else {
669+ return Err ( Error :: Other ( "got single fd, expecting dual fds" . into ( ) ) ) ;
670+ } ;
671+ let fd = tokio:: fs:: File :: from_std ( std:: fs:: File :: from ( datafd) ) ;
672+ let err = Self :: read_blob_error ( errfd) . boxed ( ) ;
673+ Ok ( ( bloblen, fd, err) )
674+ }
675+
579676 /// Fetch a descriptor. The requested size and digest are verified (by the proxy process).
580677 #[ instrument]
581678 pub async fn get_descriptor (
0 commit comments