@@ -5,9 +5,14 @@ use std::process::Command;
55use anyhow:: Result ;
66use bootc_utils:: CommandRunExt ;
77use fn_error_context:: context;
8+ use openat_ext:: OpenatDirExt ;
89use rustix:: fd:: BorrowedFd ;
910use serde:: Deserialize ;
1011
12+ use crate :: blockdev;
13+ use crate :: efi:: Efi ;
14+ use std:: path:: Path ;
15+
1116#[ derive( Deserialize , Debug ) ]
1217#[ serde( rename_all = "kebab-case" ) ]
1318#[ allow( dead_code) ]
@@ -38,3 +43,258 @@ pub(crate) fn inspect_filesystem(root: &openat::Dir, path: &str) -> Result<Files
3843 . next ( )
3944 . ok_or_else ( || anyhow:: anyhow!( "findmnt returned no data" ) )
4045}
46+
47+ #[ context( "Copying {file_path} from {src_root} to {dest_root}" ) ]
48+ pub ( crate ) fn copy_files ( src_root : & str , dest_root : & str , file_path : & str ) -> Result < ( ) > {
49+ let src_dir = openat:: Dir :: open ( src_root) ?;
50+ let file_path = file_path. strip_prefix ( "/" ) . unwrap_or ( file_path) ;
51+ let dest_dir = if file_path. starts_with ( "boot/efi" ) {
52+ let efi = Efi :: default ( ) ;
53+ match blockdev:: get_single_device ( "/" ) {
54+ Ok ( device) => {
55+ let esp_device = blockdev:: get_esp_partition ( & device) ?;
56+ let esp_path = efi. ensure_mounted_esp (
57+ Path :: new ( dest_root) ,
58+ Path :: new ( & esp_device. unwrap_or_default ( ) ) ,
59+ ) ?;
60+ openat:: Dir :: open ( & esp_path) ?
61+ }
62+ Err ( e) => anyhow:: bail!( "Unable to find device: {}" , e) ,
63+ }
64+ } else {
65+ openat:: Dir :: open ( dest_root) ?
66+ } ;
67+
68+ let src_meta = src_dir. metadata ( file_path) ?;
69+ match src_meta. simple_type ( ) {
70+ openat:: SimpleType :: File => {
71+ let parent = Path :: new ( file_path) . parent ( ) . unwrap_or ( Path :: new ( "." ) ) ;
72+ if !parent. as_os_str ( ) . is_empty ( ) {
73+ dest_dir. ensure_dir_all ( parent, 0o755 ) ?;
74+ }
75+ src_dir. copy_file_at ( file_path, & dest_dir, file_path) ?;
76+ log:: info!( "Copied file: {} to destination" , file_path) ;
77+ }
78+ openat:: SimpleType :: Dir => {
79+ anyhow:: bail!( "Unsupported copying of Directory {}" , file_path)
80+ }
81+ openat:: SimpleType :: Symlink => {
82+ anyhow:: bail!( "Unsupported symbolic link {}" , file_path)
83+ }
84+ openat:: SimpleType :: Other => {
85+ anyhow:: bail!( "Unsupported non-file/directory {}" , file_path)
86+ }
87+ }
88+
89+ Ok ( ( ) )
90+ }
91+
92+ #[ cfg( test) ]
93+ mod test {
94+ use super :: * ;
95+ use anyhow:: Result ;
96+ use openat_ext:: OpenatDirExt ;
97+ use std:: fs as std_fs;
98+ use std:: io:: Write ;
99+ use tempfile:: tempdir;
100+
101+ #[ test]
102+ fn test_copy_single_file_basic ( ) -> Result < ( ) > {
103+ let tmp = tempdir ( ) ?;
104+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
105+
106+ let src_root_name = "src_root" ;
107+ let dest_root_name = "dest_root" ;
108+
109+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
110+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
111+
112+ let src_dir = tmp_root_dir. sub_dir ( src_root_name) ?;
113+
114+ let file_to_copy_rel = "file.txt" ;
115+ let content = "This is a test file." ;
116+
117+ // Create source file using
118+ src_dir. write_file_contents ( file_to_copy_rel, 0o644 , content. as_bytes ( ) ) ?;
119+
120+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
121+ let dest_root_abs_path_str = tmp
122+ . path ( )
123+ . join ( dest_root_name)
124+ . to_str ( )
125+ . unwrap ( )
126+ . to_string ( ) ;
127+
128+ copy_files (
129+ & src_root_abs_path_str,
130+ & dest_root_abs_path_str,
131+ file_to_copy_rel,
132+ ) ?;
133+
134+ let dest_file_abs_path = tmp. path ( ) . join ( dest_root_name) . join ( file_to_copy_rel) ;
135+ assert ! ( dest_file_abs_path. exists( ) , "Destination file should exist" ) ;
136+ assert_eq ! (
137+ std_fs:: read_to_string( & dest_file_abs_path) ?,
138+ content,
139+ "File content should match"
140+ ) ;
141+
142+ Ok ( ( ) )
143+ }
144+
145+ #[ test]
146+ fn test_copy_file_in_subdirectory ( ) -> Result < ( ) > {
147+ let tmp = tempdir ( ) ?;
148+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
149+
150+ let src_root_name = "src" ;
151+ let dest_root_name = "dest" ;
152+
153+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
154+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
155+
156+ let src_dir_oat = tmp_root_dir. sub_dir ( src_root_name) ?;
157+
158+ let file_to_copy_rel = "subdir/another_file.txt" ;
159+ let content = "Content in a subdirectory." ;
160+
161+ // Create subdirectory and file in source
162+ src_dir_oat. ensure_dir_all ( "subdir" , 0o755 ) ?;
163+ let mut f = src_dir_oat. write_file ( "subdir/another_file.txt" , 0o644 ) ?;
164+ f. write_all ( content. as_bytes ( ) ) ?;
165+ f. flush ( ) ?;
166+
167+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
168+ let dest_root_abs_path_str = tmp
169+ . path ( )
170+ . join ( dest_root_name)
171+ . to_str ( )
172+ . unwrap ( )
173+ . to_string ( ) ;
174+
175+ copy_files (
176+ & src_root_abs_path_str,
177+ & dest_root_abs_path_str,
178+ file_to_copy_rel,
179+ ) ?;
180+
181+ let dest_file_abs_path = tmp. path ( ) . join ( dest_root_name) . join ( file_to_copy_rel) ;
182+ assert ! (
183+ dest_file_abs_path. exists( ) ,
184+ "Destination file in subdirectory should exist"
185+ ) ;
186+ assert_eq ! (
187+ std_fs:: read_to_string( & dest_file_abs_path) ?,
188+ content,
189+ "File content should match"
190+ ) ;
191+ assert ! (
192+ dest_file_abs_path. parent( ) . unwrap( ) . is_dir( ) ,
193+ "Destination subdirectory should be a directory"
194+ ) ;
195+
196+ Ok ( ( ) )
197+ }
198+
199+ #[ test]
200+ fn test_copy_file_with_leading_slash_in_filepath_arg ( ) -> Result < ( ) > {
201+ let tmp = tempdir ( ) ?;
202+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
203+
204+ let src_root_name = "src" ;
205+ let dest_root_name = "dest" ;
206+
207+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
208+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
209+
210+ let src_dir_oat = tmp_root_dir. sub_dir ( src_root_name) ?;
211+
212+ let file_rel_actual = "root_file.txt" ;
213+ let file_arg_with_slash = "/root_file.txt" ;
214+ let content = "Leading slash test." ;
215+
216+ src_dir_oat. write_file_contents ( file_rel_actual, 0o644 , content. as_bytes ( ) ) ?;
217+
218+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
219+ let dest_root_abs_path_str = tmp
220+ . path ( )
221+ . join ( dest_root_name)
222+ . to_str ( )
223+ . unwrap ( )
224+ . to_string ( ) ;
225+
226+ copy_files (
227+ & src_root_abs_path_str,
228+ & dest_root_abs_path_str,
229+ file_arg_with_slash,
230+ ) ?;
231+
232+ // The destination path should be based on the path *after* stripping the leading slash
233+ let dest_file_abs_path = tmp. path ( ) . join ( dest_root_name) . join ( file_rel_actual) ;
234+ assert ! (
235+ dest_file_abs_path. exists( ) ,
236+ "Destination file should exist despite leading slash in arg"
237+ ) ;
238+ assert_eq ! (
239+ std_fs:: read_to_string( & dest_file_abs_path) ?,
240+ content,
241+ "File content should match"
242+ ) ;
243+
244+ Ok ( ( ) )
245+ }
246+
247+ #[ test]
248+ fn test_copy_fails_for_directory ( ) -> Result < ( ) > {
249+ let tmp = tempdir ( ) ?;
250+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
251+
252+ let src_root_name = "src" ;
253+ let dest_root_name = "dest" ;
254+
255+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
256+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
257+
258+ let src_dir_oat = tmp_root_dir. sub_dir ( src_root_name) ?;
259+
260+ let dir_to_copy_rel = "a_directory" ;
261+ src_dir_oat. create_dir ( dir_to_copy_rel, 0o755 ) ?; // Create the directory in the source
262+
263+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
264+ let dest_root_abs_path_str = tmp
265+ . path ( )
266+ . join ( dest_root_name)
267+ . to_str ( )
268+ . unwrap ( )
269+ . to_string ( ) ;
270+
271+ let result = copy_files (
272+ & src_root_abs_path_str,
273+ & dest_root_abs_path_str,
274+ dir_to_copy_rel,
275+ ) ;
276+
277+ assert ! ( result. is_err( ) , "Copying a directory should fail." ) ;
278+ if let Err ( e) = result {
279+ let mut found_unsupported_message = false ;
280+ for cause in e. chain ( ) {
281+ // Iterate through the error chain
282+ if cause
283+ . to_string ( )
284+ . contains ( "Unsupported copying of Directory" )
285+ {
286+ found_unsupported_message = true ;
287+ break ;
288+ }
289+ }
290+ assert ! (
291+ found_unsupported_message,
292+ "The error chain should contain 'Unsupported copying of Directory'. Full error: {:#?}" ,
293+ e
294+ ) ;
295+ } else {
296+ panic ! ( "Expected an error when attempting to copy a directory, but got Ok." ) ;
297+ }
298+ Ok ( ( ) )
299+ }
300+ }
0 commit comments