@@ -109,6 +109,12 @@ pub struct DftArgs {
109109 ) ]
110110 pub header : Option < Vec < ( String , String ) > > ,
111111
112+ #[ clap(
113+ long,
114+ help = "Path to file containing Flight SQL headers. Supports simple format ('Name: Value') and curl config format ('header = Name: Value' or '-H \" Name: Value\" '). Only used for FlightSQL"
115+ ) ]
116+ pub headers_file : Option < PathBuf > ,
117+
112118 #[ clap(
113119 long,
114120 short,
@@ -257,7 +263,7 @@ fn parse_command(command: &str) -> std::result::Result<String, String> {
257263fn parse_header_line ( line : & str ) -> Result < ( String , String ) , String > {
258264 let ( name, value) = line
259265 . split_once ( ':' )
260- . ok_or_else ( || format ! ( "Invalid header format: '{}'" , line) ) ?;
266+ . ok_or_else ( || format ! ( "Invalid header format: '{}'\n Expected format: 'Header-Name: Header-Value', 'header = Name: Value', or '-H \" Name: Value \" ' " , line) ) ?;
261267
262268 let name =
263269 HeaderName :: try_from ( name. trim ( ) ) . map_err ( |e| format ! ( "Invalid header name: {}" , e) ) ?;
@@ -270,3 +276,213 @@ fn parse_header_line(line: &str) -> Result<(String, String), String> {
270276
271277 Ok ( ( name. to_string ( ) , value_str. to_string ( ) ) )
272278}
279+
280+ /// Parse headers from a file supporting both simple and curl config formats
281+ ///
282+ /// Supported formats:
283+ /// - Simple: `Header-Name: Header-Value`
284+ /// - Curl config: `header = Name: Value` or `-H "Name: Value"`
285+ /// - Comments: Lines starting with `#`
286+ /// - Blank lines are ignored
287+ ///
288+ /// Both formats can be mixed in the same file.
289+ pub fn parse_headers_file ( path : & Path ) -> Result < Vec < ( String , String ) > , String > {
290+ let content = std:: fs:: read_to_string ( path)
291+ . map_err ( |e| format ! ( "Failed to read headers file '{}': {}" , path. display( ) , e) ) ?;
292+
293+ let mut headers = Vec :: new ( ) ;
294+ for ( line_num, line) in content. lines ( ) . enumerate ( ) {
295+ let line = line. trim ( ) ;
296+
297+ // Skip comments and blank lines
298+ if line. is_empty ( ) || line. starts_with ( '#' ) {
299+ continue ;
300+ }
301+
302+ // Detect and parse format
303+ let header_value = if let Some ( stripped) = line. strip_prefix ( "header" ) {
304+ // Curl config format: "header = Name: Value" or "header=Name: Value"
305+ let stripped = stripped. trim_start ( ) ;
306+ if let Some ( value) = stripped. strip_prefix ( '=' ) {
307+ value. trim ( )
308+ } else {
309+ line // Not curl format, try simple format
310+ }
311+ } else if let Some ( stripped) = line. strip_prefix ( "-H" ) {
312+ // Curl config format: -H "Name: Value" or -H Name: Value
313+ let stripped = stripped. trim ( ) ;
314+ // Remove surrounding quotes if present
315+ stripped. trim_matches ( |c| c == '"' || c == '\'' )
316+ } else {
317+ // Simple format: "Name: Value"
318+ line
319+ } ;
320+
321+ // Parse header line
322+ match parse_header_line ( header_value) {
323+ Ok ( header) => headers. push ( header) ,
324+ Err ( e) => {
325+ return Err ( format ! (
326+ "Invalid header format at line {} in '{}': '{}'\n {}" ,
327+ line_num + 1 ,
328+ path. display( ) ,
329+ line,
330+ e
331+ ) ) ;
332+ }
333+ }
334+ }
335+
336+ Ok ( headers)
337+ }
338+
339+ #[ cfg( test) ]
340+ mod tests {
341+ use super :: * ;
342+ use std:: io:: Write ;
343+ use tempfile:: NamedTempFile ;
344+
345+ #[ test]
346+ fn test_parse_headers_file_simple_format ( ) {
347+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
348+ writeln ! ( file, "x-api-key: secret123" ) . unwrap ( ) ;
349+ writeln ! ( file, "database: production" ) . unwrap ( ) ;
350+ file. flush ( ) . unwrap ( ) ;
351+
352+ let headers = parse_headers_file ( file. path ( ) ) . unwrap ( ) ;
353+ assert_eq ! ( headers. len( ) , 2 ) ;
354+ assert_eq ! (
355+ headers[ 0 ] ,
356+ ( "x-api-key" . to_string( ) , "secret123" . to_string( ) )
357+ ) ;
358+ assert_eq ! (
359+ headers[ 1 ] ,
360+ ( "database" . to_string( ) , "production" . to_string( ) )
361+ ) ;
362+ }
363+
364+ #[ test]
365+ fn test_parse_headers_file_curl_format ( ) {
366+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
367+ writeln ! ( file, "header = x-api-key: secret123" ) . unwrap ( ) ;
368+ writeln ! ( file, "-H \" database: production\" " ) . unwrap ( ) ;
369+ file. flush ( ) . unwrap ( ) ;
370+
371+ let headers = parse_headers_file ( file. path ( ) ) . unwrap ( ) ;
372+ assert_eq ! ( headers. len( ) , 2 ) ;
373+ assert_eq ! (
374+ headers[ 0 ] ,
375+ ( "x-api-key" . to_string( ) , "secret123" . to_string( ) )
376+ ) ;
377+ assert_eq ! (
378+ headers[ 1 ] ,
379+ ( "database" . to_string( ) , "production" . to_string( ) )
380+ ) ;
381+ }
382+
383+ #[ test]
384+ fn test_parse_headers_file_mixed_format ( ) {
385+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
386+ writeln ! ( file, "# Simple format" ) . unwrap ( ) ;
387+ writeln ! ( file, "x-test: value1" ) . unwrap ( ) ;
388+ writeln ! ( file, "" ) . unwrap ( ) ;
389+ writeln ! ( file, "# Curl config format" ) . unwrap ( ) ;
390+ writeln ! ( file, "header = x-api-key: secret123" ) . unwrap ( ) ;
391+ writeln ! ( file, "-H \" database: production\" " ) . unwrap ( ) ;
392+ file. flush ( ) . unwrap ( ) ;
393+
394+ let headers = parse_headers_file ( file. path ( ) ) . unwrap ( ) ;
395+ assert_eq ! ( headers. len( ) , 3 ) ;
396+ assert_eq ! ( headers[ 0 ] , ( "x-test" . to_string( ) , "value1" . to_string( ) ) ) ;
397+ assert_eq ! (
398+ headers[ 1 ] ,
399+ ( "x-api-key" . to_string( ) , "secret123" . to_string( ) )
400+ ) ;
401+ assert_eq ! (
402+ headers[ 2 ] ,
403+ ( "database" . to_string( ) , "production" . to_string( ) )
404+ ) ;
405+ }
406+
407+ #[ test]
408+ fn test_parse_headers_file_with_comments ( ) {
409+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
410+ writeln ! ( file, "# This is a comment" ) . unwrap ( ) ;
411+ writeln ! ( file, "x-api-key: secret123" ) . unwrap ( ) ;
412+ writeln ! ( file, "# Another comment" ) . unwrap ( ) ;
413+ writeln ! ( file, "database: production" ) . unwrap ( ) ;
414+ file. flush ( ) . unwrap ( ) ;
415+
416+ let headers = parse_headers_file ( file. path ( ) ) . unwrap ( ) ;
417+ assert_eq ! ( headers. len( ) , 2 ) ;
418+ assert_eq ! (
419+ headers[ 0 ] ,
420+ ( "x-api-key" . to_string( ) , "secret123" . to_string( ) )
421+ ) ;
422+ assert_eq ! (
423+ headers[ 1 ] ,
424+ ( "database" . to_string( ) , "production" . to_string( ) )
425+ ) ;
426+ }
427+
428+ #[ test]
429+ fn test_parse_headers_file_blank_lines ( ) {
430+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
431+ writeln ! ( file, "x-api-key: secret123" ) . unwrap ( ) ;
432+ writeln ! ( file, "" ) . unwrap ( ) ;
433+ writeln ! ( file, " " ) . unwrap ( ) ;
434+ writeln ! ( file, "database: production" ) . unwrap ( ) ;
435+ file. flush ( ) . unwrap ( ) ;
436+
437+ let headers = parse_headers_file ( file. path ( ) ) . unwrap ( ) ;
438+ assert_eq ! ( headers. len( ) , 2 ) ;
439+ assert_eq ! (
440+ headers[ 0 ] ,
441+ ( "x-api-key" . to_string( ) , "secret123" . to_string( ) )
442+ ) ;
443+ assert_eq ! (
444+ headers[ 1 ] ,
445+ ( "database" . to_string( ) , "production" . to_string( ) )
446+ ) ;
447+ }
448+
449+ #[ test]
450+ fn test_parse_headers_file_curl_with_quotes ( ) {
451+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
452+ writeln ! ( file, "-H \" x-api-key: secret123\" " ) . unwrap ( ) ;
453+ writeln ! ( file, "-H 'database: production'" ) . unwrap ( ) ;
454+ file. flush ( ) . unwrap ( ) ;
455+
456+ let headers = parse_headers_file ( file. path ( ) ) . unwrap ( ) ;
457+ assert_eq ! ( headers. len( ) , 2 ) ;
458+ assert_eq ! (
459+ headers[ 0 ] ,
460+ ( "x-api-key" . to_string( ) , "secret123" . to_string( ) )
461+ ) ;
462+ assert_eq ! (
463+ headers[ 1 ] ,
464+ ( "database" . to_string( ) , "production" . to_string( ) )
465+ ) ;
466+ }
467+
468+ #[ test]
469+ fn test_parse_headers_file_invalid_format ( ) {
470+ let mut file = NamedTempFile :: new ( ) . unwrap ( ) ;
471+ writeln ! ( file, "x-api-key: secret123" ) . unwrap ( ) ;
472+ writeln ! ( file, "invalid-line-without-colon" ) . unwrap ( ) ;
473+ file. flush ( ) . unwrap ( ) ;
474+
475+ let result = parse_headers_file ( file. path ( ) ) ;
476+ assert ! ( result. is_err( ) ) ;
477+ assert ! ( result
478+ . unwrap_err( )
479+ . contains( "Invalid header format at line 2" ) ) ;
480+ }
481+
482+ #[ test]
483+ fn test_parse_headers_file_not_found ( ) {
484+ let result = parse_headers_file ( Path :: new ( "/nonexistent/file.txt" ) ) ;
485+ assert ! ( result. is_err( ) ) ;
486+ assert ! ( result. unwrap_err( ) . contains( "Failed to read headers file" ) ) ;
487+ }
488+ }
0 commit comments