11use std:: io:: { BufRead , Read } ;
22use std:: path:: { Path , PathBuf } ;
33
4- use anyhow:: Error ;
4+ use anyhow:: { Result , bail } ;
55use quick_xml:: escape:: unescape;
66use quick_xml:: events:: { BytesStart , Event } ;
77
88use crate :: utils:: attributes:: get_attributes;
99use crate :: utils:: file_utils:: { escape_filename, join_scope_id_and_name} ;
1010use crate :: utils:: xml_utils:: {
11- XmlEventType , end_element_to_string, end_element_to_string_from_start_element,
12- extract_values_from_xml_paths, push_rest_of_element_to_skeleton, skip_rest_of_element,
13- start_element_to_string,
11+ XmlEventType , end_element_to_string, extract_values_from_xml_paths, format_end_tag,
12+ push_rest_of_element_to_skeleton, skip_rest_of_element, start_element_to_string,
1413} ;
1514use crate :: utils:: {
16- FolderStructure , build_out_dir_path, create_dir, move_to_subfolder,
17- write_rest_of_element_to_file,
15+ FolderStructure , build_out_dir_path, create_dir, move_to_subfolder, push_line_to_skeleton ,
16+ rename_file_if_necessary , write_rest_of_element_to_file,
1817} ;
19- use crate :: utils:: { push_line_to_skeleton, rename_file_if_necessary} ;
2018use crate :: xml_processor:: ProcessingContext ;
2119
2220/// Parse and explode a generic catalog, handling both wrapped and unwrapped formats
2321pub fn xml_explode_catalog < R : Read + BufRead > (
2422 context : & mut ProcessingContext < ' _ , R > ,
25- _start_tag : & BytesStart ,
2623 folder_structure : Option < & FolderStructure > ,
27- // catalog_config: &CatalogConfig,
28- ) -> Result < Option < FolderStructure > , Error > {
29- let catalog_type = match context. catalog_type {
30- Some ( catalog_type) => catalog_type,
31- None => return Err ( anyhow:: anyhow!( "❌ Catalog type not specified" ) ) ,
24+ ) -> Result < Option < FolderStructure > > {
25+ let Some ( catalog_type) = context. catalog_type else {
26+ bail ! ( "Catalog type not specified" ) ;
3227 } ;
3328 let catalog_config = catalog_type. get_config ( ) ;
34- let catalog_item_name = catalog_config. catalog_item_name . clone ( ) ;
29+ let catalog_item_name = catalog_config. catalog_item_name ;
3530 let wrapped_in_object_list = catalog_config. wrapped_in_object_list ;
3631 let uses_folders = catalog_config. uses_folders ;
3732 let id_path = & catalog_config. id_path ;
3833
3934 let out_dir_path_base = build_out_dir_path ( context, None ) ?;
4035 create_dir ( & out_dir_path_base) ;
4136
42- let mut buf = Vec :: new ( ) ; // buffer for reading xml events
37+ let mut buf = Vec :: new ( ) ;
4338
4439 // Adjust depth based on whether items are wrapped in ObjectList
4540 let base_depth = context. path_stack . len ( ) ; // depth of the catalog start tag, e.g., 4 for BaseDirectoryCatalog, if the path is FMSaveAsXML/Structure/AddAction/BaseDirectoryCatalog
@@ -53,7 +48,7 @@ pub fn xml_explode_catalog<R: Read + BufRead>(
5348
5449 // We'll build a folder structure for custom functions, layouts, scripts, etc.
5550 let mut folder_structure_result = if uses_folders {
56- Some ( FolderStructure :: new ( ) )
51+ Some ( FolderStructure :: default ( ) )
5752 } else {
5853 None
5954 } ;
@@ -82,17 +77,16 @@ pub fn xml_explode_catalog<R: Read + BufRead>(
8277
8378 // Is the current element an ancillary element?
8479 // Ancillary elements are elements that are not catalog items or <ObjectList>, e.g., <UUID> or <TagList>
85- let is_ancillary_element = rel_depth == 2
86- && (
87- // Siblings of ObjectList wrapper at depth 2
88- ( wrapped_in_object_list && e. name ( ) . as_ref ( ) != b"ObjectList" )
89- // When not wrapped in <ObjectList>, siblings of catalog items
90- || ( !wrapped_in_object_list && e. name ( ) . as_ref ( ) != catalog_item_name)
91- ) ;
80+ let expected_element = if wrapped_in_object_list {
81+ b"ObjectList" . as_ref ( )
82+ } else {
83+ catalog_item_name
84+ } ;
85+ let is_ancillary_element = rel_depth == 2 && e. name ( ) . as_ref ( ) != expected_element;
9286
9387 // Handle ancillary elements like <UUID> and <TagList>
9488 if is_ancillary_element {
95- handle_ancillary_element ( context, & e , base_depth, rel_depth) ;
89+ handle_ancillary_element ( context, base_depth, rel_depth) ;
9690 rel_depth -= 1 ;
9791 }
9892
@@ -104,23 +98,24 @@ pub fn xml_explode_catalog<R: Read + BufRead>(
10498 // Handle catalog items (e.g. <BaseDirectory>)
10599 // Parse attributes needed for folder tracking
106100 if uses_folders {
107- let ( id, name, is_folder, is_marker, is_separator) =
108- parse_folder_attributes ( & e) ;
109- current_id = id;
110- current_name = name;
101+ let attrs = parse_folder_attributes ( & e) ;
102+ current_id = attrs. id ;
103+ current_name = attrs. name ;
111104
112- if is_folder {
105+ if attrs . is_folder {
113106 current_path. push ( join_scope_id_and_name (
114107 & current_id,
115108 & escape_filename ( & current_name) ,
116109 ) ) ;
117110 }
118- if is_marker {
111+ if attrs . is_marker {
119112 current_path. pop ( ) ;
120113 }
121114
122- if ( is_folder || is_marker || is_separator) && !context. flags . lossless {
123- skip_rest_of_element ( context. reader , & e) ;
115+ if ( attrs. is_folder || attrs. is_marker || attrs. is_separator )
116+ && !context. flags . lossless
117+ {
118+ skip_rest_of_element ( context. reader ) ;
124119 rel_depth -= 1 ;
125120 continue ;
126121 }
@@ -139,30 +134,26 @@ pub fn xml_explode_catalog<R: Read + BufRead>(
139134 base_depth + rel_depth - 1 ,
140135 id_path,
141136 ) ;
142- rename_file_if_necessary ( & file_path, context. path_stack , & catalog_item_name) ;
137+ rename_file_if_necessary ( & file_path, context. path_stack , catalog_item_name) ;
143138
144139 // Move to subfolder if necessary
145- let subfolder_dir_path = determine_subfolder_path (
140+ if let Some ( subfolder ) = determine_subfolder_path (
146141 & out_dir_path_base,
147142 folder_structure,
148143 & file_path,
149144 id_path,
150145 uses_folders,
151146 & current_path,
152- ) ;
153- if let Some ( subfolder_dir_path) = subfolder_dir_path
154- && subfolder_dir_path != out_dir_path_base
155- && !subfolder_dir_path. to_string_lossy ( ) . is_empty ( )
156- {
157- let _ = move_to_subfolder ( & file_path, & subfolder_dir_path) ;
147+ ) {
148+ let _ = move_to_subfolder ( & file_path, & subfolder) ;
158149 }
159150
160151 update_folder_structure ( & mut folder_structure_result, & current_id, & current_path) ;
161152
162153 // The element will be consumed by now, so we can't rely on catching the end tag in the Ok(Event::End) arm below
163154 // Instead, write it out manually here
164155 if context. flags . lossless {
165- let end_tag = end_element_to_string_from_start_element ( & e ) ;
156+ let end_tag = format_end_tag ( e . name ( ) . as_ref ( ) ) ;
166157 push_line_to_skeleton (
167158 context. skeleton ,
168159 base_depth,
@@ -199,7 +190,7 @@ pub fn xml_explode_catalog<R: Read + BufRead>(
199190 _ => { }
200191 }
201192
202- buf. clear ( )
193+ buf. clear ( ) ;
203194 }
204195
205196 Ok ( folder_structure_result)
@@ -213,59 +204,58 @@ fn add_start_tag_to_skeleton<R: Read + BufRead>(
213204 rel_depth : usize ,
214205 wrapped_in_object_list : bool ,
215206) {
216- if !context. flags . lossless {
217- return ;
218- }
219-
220- let should_add_to_skeleton = rel_depth == 2 || ( wrapped_in_object_list && rel_depth == 3 ) ;
221- if !should_add_to_skeleton {
207+ let should_add = rel_depth == 2 || ( wrapped_in_object_list && rel_depth == 3 ) ;
208+ if !context. flags . lossless || !should_add {
222209 return ;
223210 }
224211
212+ // When should_add is true, the element is always a child of the catalog:
213+ // depth 2 is always a child, and depth 3 only passes when wrapped_in_object_list
225214 push_line_to_skeleton (
226215 context. skeleton ,
227216 base_depth,
228217 rel_depth,
229218 start_tag,
230- if wrapped_in_object_list {
231- rel_depth >= 2
232- } else {
233- rel_depth == 2
234- } ,
219+ true ,
235220 XmlEventType :: Start ,
236221 ) ;
237222}
238223
224+ #[ derive( Default ) ]
225+ struct FolderAttributes {
226+ id : String ,
227+ name : String ,
228+ is_folder : bool ,
229+ is_marker : bool ,
230+ is_separator : bool ,
231+ }
232+
239233/// Parse folder-related attributes from a catalog item
240- fn parse_folder_attributes ( e : & BytesStart ) -> ( String , String , bool , bool , bool ) {
241- let mut current_id = String :: new ( ) ;
242- let mut current_name = String :: new ( ) ;
243- let mut is_folder = false ;
244- let mut is_marker = false ;
245- let mut is_separator = false ;
234+ fn parse_folder_attributes ( e : & BytesStart ) -> FolderAttributes {
235+ let mut attrs = FolderAttributes :: default ( ) ;
246236
247- for attr in get_attributes ( e) . unwrap ( ) {
237+ for attr in get_attributes ( e) {
248238 match attr. 0 . as_str ( ) {
249- "id" => current_id = attr. 1 . to_string ( ) ,
250- "name" => current_name = unescape ( attr. 1 . as_str ( ) ) . unwrap ( ) . to_string ( ) ,
239+ "id" => attrs . id = attr. 1 ,
240+ "name" => attrs . name = unescape ( & attr. 1 ) . unwrap ( ) . into_owned ( ) ,
251241 "isFolder" => match attr. 1 . as_str ( ) {
252- "True" => is_folder = true ,
253- "Marker" => is_marker = true ,
242+ "True" => attrs . is_folder = true ,
243+ "Marker" => attrs . is_marker = true ,
254244 _ => { }
255245 } ,
256- "isSeparatorItem" => {
257- if attr. 1 . as_str ( ) == "True" {
258- is_separator = true
259- }
260- }
246+ "isSeparatorItem" => attrs. is_separator = attr. 1 == "True" ,
261247 _ => { }
262248 }
263249 }
264250
265- ( current_id , current_name , is_folder , is_marker , is_separator )
251+ attrs
266252}
267253
268- /// Determine the subfolder path for a catalog item
254+ /// Determine the subfolder path for a catalog item.
255+ /// Uses `current_path` for catalogs that track their own folder hierarchy (scripts, custom functions, layouts),
256+ /// or looks up the path via `folder_structure` for dependent catalogs (steps, calcs).
257+ /// Returns `Some(subfolder)` only when the item should be moved, i.e., the resolved
258+ /// subfolder differs from `out_dir_path_base`.
269259fn determine_subfolder_path (
270260 out_dir_path_base : & Path ,
271261 folder_structure : Option < & FolderStructure > ,
@@ -274,59 +264,47 @@ fn determine_subfolder_path(
274264 uses_folders : bool ,
275265 current_path : & [ String ] ,
276266) -> Option < PathBuf > {
277- let folder_structure = match folder_structure {
278- Some ( folder_structure) => folder_structure,
267+ let subfolder = match folder_structure {
268+ Some ( fs) => {
269+ // For dependent catalogs that use a previously-built folder structure
270+ if id_path. is_empty ( ) {
271+ return None ;
272+ }
273+ let results = extract_values_from_xml_paths ( file_path, & [ id_path] ) . ok ( ) ?;
274+ let id = results. first ( ) ?. as_ref ( ) ?;
275+ let path = fs. get_path_for_id ( id) ;
276+ if path. is_empty ( ) {
277+ return None ;
278+ }
279+ out_dir_path_base. join ( path. join ( "/" ) )
280+ }
279281 None => {
280- return if uses_folders && !current_path. is_empty ( ) {
281- // Track folders using current catalog item
282- Some ( out_dir_path_base. join ( current_path. join ( "/" ) ) )
283- } else {
284- None
285- } ;
282+ // For catalogs with their own folder tracking
283+ if !uses_folders || current_path. is_empty ( ) {
284+ return None ;
285+ }
286+ out_dir_path_base. join ( current_path. join ( "/" ) )
286287 }
287288 } ;
288289
289- // If a folder structure was provided, use that
290- if id_path. is_empty ( ) {
291- return None ;
292- }
293-
294- let paths = vec ! [ id_path] ;
295- let results = match extract_values_from_xml_paths ( file_path, & paths) {
296- Ok ( results) => results,
297- Err ( _) => return None ,
298- } ;
299-
300- let id = match results. first ( ) {
301- Some ( Some ( id) ) => id,
302- _ => return None ,
303- } ;
304-
305- let function_path = folder_structure. get_path_for_id ( id) ;
306- if function_path. is_empty ( ) {
307- return None ;
308- }
309-
310- Some ( out_dir_path_base. join ( function_path. join ( "/" ) ) )
290+ ( subfolder != out_dir_path_base) . then_some ( subfolder)
311291}
312292
313293/// Handle ancillary elements like <UUID> and <TagList>
314294fn handle_ancillary_element < R : Read + BufRead > (
315295 context : & mut ProcessingContext < ' _ , R > ,
316- e : & BytesStart ,
317296 base_depth : usize ,
318297 rel_depth : usize ,
319298) {
320299 if context. flags . lossless {
321300 push_rest_of_element_to_skeleton (
322301 context. reader ,
323- e,
324302 context. skeleton ,
325303 base_depth + rel_depth - 1 ,
326304 context. flags ,
327305 ) ;
328306 } else {
329- skip_rest_of_element ( context. reader , e ) ;
307+ skip_rest_of_element ( context. reader ) ;
330308 }
331309}
332310
0 commit comments