@@ -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,260 @@ 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+ // ... existing code in filesystem.rs ...
93+
94+ #[ cfg( test) ]
95+ mod test {
96+ use super :: * ;
97+ use anyhow:: Result ;
98+ use openat_ext:: OpenatDirExt ;
99+ use std:: fs as std_fs;
100+ use std:: io:: Write ;
101+ use tempfile:: tempdir;
102+
103+ #[ test]
104+ fn test_copy_single_file_basic ( ) -> Result < ( ) > {
105+ let tmp = tempdir ( ) ?;
106+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
107+
108+ let src_root_name = "src_root" ;
109+ let dest_root_name = "dest_root" ;
110+
111+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
112+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
113+
114+ let src_dir = tmp_root_dir. sub_dir ( src_root_name) ?;
115+
116+ let file_to_copy_rel = "file.txt" ;
117+ let content = "This is a test file." ;
118+
119+ // Create source file using
120+ src_dir. write_file_contents ( file_to_copy_rel, 0o644 , content. as_bytes ( ) ) ?;
121+
122+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
123+ let dest_root_abs_path_str = tmp
124+ . path ( )
125+ . join ( dest_root_name)
126+ . to_str ( )
127+ . unwrap ( )
128+ . to_string ( ) ;
129+
130+ copy_files (
131+ & src_root_abs_path_str,
132+ & dest_root_abs_path_str,
133+ file_to_copy_rel,
134+ ) ?;
135+
136+ let dest_file_abs_path = tmp. path ( ) . join ( dest_root_name) . join ( file_to_copy_rel) ;
137+ assert ! ( dest_file_abs_path. exists( ) , "Destination file should exist" ) ;
138+ assert_eq ! (
139+ std_fs:: read_to_string( & dest_file_abs_path) ?,
140+ content,
141+ "File content should match"
142+ ) ;
143+
144+ Ok ( ( ) )
145+ }
146+
147+ #[ test]
148+ fn test_copy_file_in_subdirectory ( ) -> Result < ( ) > {
149+ let tmp = tempdir ( ) ?;
150+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
151+
152+ let src_root_name = "src" ;
153+ let dest_root_name = "dest" ;
154+
155+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
156+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
157+
158+ let src_dir_oat = tmp_root_dir. sub_dir ( src_root_name) ?;
159+
160+ let file_to_copy_rel = "subdir/another_file.txt" ;
161+ let content = "Content in a subdirectory." ;
162+
163+ // Create subdirectory and file in source
164+ src_dir_oat. ensure_dir_all ( "subdir" , 0o755 ) ?;
165+ let mut f = src_dir_oat. write_file ( "subdir/another_file.txt" , 0o644 ) ?;
166+ f. write_all ( content. as_bytes ( ) ) ?;
167+ f. flush ( ) ?;
168+
169+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
170+ let dest_root_abs_path_str = tmp
171+ . path ( )
172+ . join ( dest_root_name)
173+ . to_str ( )
174+ . unwrap ( )
175+ . to_string ( ) ;
176+
177+ copy_files (
178+ & src_root_abs_path_str,
179+ & dest_root_abs_path_str,
180+ file_to_copy_rel,
181+ ) ?;
182+
183+ let dest_file_abs_path = tmp. path ( ) . join ( dest_root_name) . join ( file_to_copy_rel) ;
184+ assert ! (
185+ dest_file_abs_path. exists( ) ,
186+ "Destination file in subdirectory should exist"
187+ ) ;
188+ assert_eq ! (
189+ std_fs:: read_to_string( & dest_file_abs_path) ?,
190+ content,
191+ "File content should match"
192+ ) ;
193+ assert ! (
194+ dest_file_abs_path. parent( ) . unwrap( ) . is_dir( ) ,
195+ "Destination subdirectory should be a directory"
196+ ) ;
197+
198+ Ok ( ( ) )
199+ }
200+
201+ #[ test]
202+ fn test_copy_file_with_leading_slash_in_filepath_arg ( ) -> Result < ( ) > {
203+ let tmp = tempdir ( ) ?;
204+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
205+
206+ let src_root_name = "src" ;
207+ let dest_root_name = "dest" ;
208+
209+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
210+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
211+
212+ let src_dir_oat = tmp_root_dir. sub_dir ( src_root_name) ?;
213+
214+ let file_rel_actual = "root_file.txt" ;
215+ let file_arg_with_slash = "/root_file.txt" ;
216+ let content = "Leading slash test." ;
217+
218+ src_dir_oat. write_file_contents ( file_rel_actual, 0o644 , content. as_bytes ( ) ) ?;
219+
220+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
221+ let dest_root_abs_path_str = tmp
222+ . path ( )
223+ . join ( dest_root_name)
224+ . to_str ( )
225+ . unwrap ( )
226+ . to_string ( ) ;
227+
228+ copy_files (
229+ & src_root_abs_path_str,
230+ & dest_root_abs_path_str,
231+ file_arg_with_slash,
232+ ) ?;
233+
234+ // The destination path should be based on the path *after* stripping the leading slash
235+ let dest_file_abs_path = tmp. path ( ) . join ( dest_root_name) . join ( file_rel_actual) ;
236+ assert ! (
237+ dest_file_abs_path. exists( ) ,
238+ "Destination file should exist despite leading slash in arg"
239+ ) ;
240+ assert_eq ! (
241+ std_fs:: read_to_string( & dest_file_abs_path) ?,
242+ content,
243+ "File content should match"
244+ ) ;
245+
246+ Ok ( ( ) )
247+ }
248+
249+ #[ test]
250+ fn test_copy_fails_for_directory ( ) -> Result < ( ) > {
251+ let tmp = tempdir ( ) ?;
252+ let tmp_root_dir = openat:: Dir :: open ( tmp. path ( ) ) ?;
253+
254+ let src_root_name = "src" ;
255+ let dest_root_name = "dest" ;
256+
257+ tmp_root_dir. create_dir ( src_root_name, 0o755 ) ?;
258+ tmp_root_dir. create_dir ( dest_root_name, 0o755 ) ?;
259+
260+ let src_dir_oat = tmp_root_dir. sub_dir ( src_root_name) ?;
261+
262+ let dir_to_copy_rel = "a_directory" ;
263+ src_dir_oat. create_dir ( dir_to_copy_rel, 0o755 ) ?; // Create the directory in the source
264+
265+ let src_root_abs_path_str = tmp. path ( ) . join ( src_root_name) . to_str ( ) . unwrap ( ) . to_string ( ) ;
266+ let dest_root_abs_path_str = tmp
267+ . path ( )
268+ . join ( dest_root_name)
269+ . to_str ( )
270+ . unwrap ( )
271+ . to_string ( ) ;
272+
273+ let result = copy_files (
274+ & src_root_abs_path_str,
275+ & dest_root_abs_path_str,
276+ dir_to_copy_rel,
277+ ) ;
278+
279+ assert ! ( result. is_err( ) , "Copying a directory should fail." ) ;
280+ if let Err ( e) = result {
281+ let mut found_unsupported_message = false ;
282+ for cause in e. chain ( ) {
283+ // Iterate through the error chain
284+ if cause
285+ . to_string ( )
286+ . contains ( "Unsupported copying of Directory" )
287+ {
288+ found_unsupported_message = true ;
289+ break ;
290+ }
291+ }
292+ assert ! (
293+ found_unsupported_message,
294+ "The error chain should contain 'Unsupported copying of Directory'. Full error: {:#?}" ,
295+ e
296+ ) ;
297+ } else {
298+ panic ! ( "Expected an error when attempting to copy a directory, but got Ok." ) ;
299+ }
300+ Ok ( ( ) )
301+ }
302+ }
0 commit comments