@@ -89,41 +89,55 @@ fn is_pdf_path(path: &Path) -> bool {
8989}
9090
9191pub fn load_api_key ( env_file : & Path ) -> Result < String > {
92+ let env_file_exists = env_file. try_exists ( ) . with_context ( || {
93+ format ! (
94+ "Failed to access env file metadata: {}" ,
95+ env_file. display( )
96+ )
97+ } ) ?;
98+
99+ if env_file_exists {
100+ let entries = dotenvy:: from_path_iter ( env_file) . with_context ( || {
101+ format ! (
102+ "Failed to read or parse env file: {}" ,
103+ env_file. display( )
104+ )
105+ } ) ?;
106+ let mut file_key = None ;
107+ for entry in entries {
108+ let ( key, value) = entry. with_context ( || {
109+ format ! (
110+ "Failed to read or parse env file: {}" ,
111+ env_file. display( )
112+ )
113+ } ) ?;
114+ if key == "ZAI_API_KEY" {
115+ if value. trim ( ) . is_empty ( ) {
116+ file_key = None ;
117+ } else {
118+ file_key = Some ( value) ;
119+ }
120+ }
121+ }
122+ if let Some ( key) = file_key {
123+ return Ok ( key) ;
124+ }
125+ }
126+
92127 if let Ok ( api_key) = std:: env:: var ( "ZAI_API_KEY" )
93128 && !api_key. trim ( ) . is_empty ( )
94129 {
95130 return Ok ( api_key) ;
96131 }
97132
98- let content = std :: fs :: read_to_string ( env_file ) . with_context ( || {
99- format ! (
133+ if !env_file_exists {
134+ return Err ( anyhow ! (
100135 "ZAI_API_KEY is not set and env file was not found: {}" ,
101136 env_file. display( )
102- )
103- } ) ?;
104-
105- for line in content. lines ( ) {
106- let stripped = line. trim ( ) ;
107- if stripped. is_empty ( ) || stripped. starts_with ( '#' ) || !stripped. contains ( '=' ) {
108- continue ;
109- }
110- let mut split = stripped. splitn ( 2 , '=' ) ;
111- let key = split. next ( ) . unwrap_or_default ( ) . trim ( ) ;
112- let value = split
113- . next ( )
114- . unwrap_or_default ( )
115- . trim ( )
116- . trim_matches ( '"' )
117- . trim_matches ( '\'' ) ;
118- if key == "ZAI_API_KEY" && !value. is_empty ( ) {
119- return Ok ( value. to_string ( ) ) ;
120- }
137+ ) ) ;
121138 }
122139
123- Err ( anyhow ! (
124- "ZAI_API_KEY was not found in {}" ,
125- env_file. display( )
126- ) )
140+ Err ( anyhow ! ( "ZAI_API_KEY was not found in {}" , env_file. display( ) ) )
127141}
128142
129143pub async fn process_pdf (
@@ -701,6 +715,119 @@ mod tests {
701715 assert ! ( err. contains( "ZAI_API_KEY was not found" ) ) ;
702716 }
703717
718+ #[ test]
719+ fn load_api_key_missing_file_falls_back_to_environment ( ) {
720+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
721+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
722+ let missing = tmp. path ( ) . join ( "missing.env" ) ;
723+ unsafe {
724+ std:: env:: set_var ( "ZAI_API_KEY" , "env-fallback-key" ) ;
725+ }
726+ let key = load_api_key ( & missing) . unwrap ( ) ;
727+ unsafe {
728+ std:: env:: remove_var ( "ZAI_API_KEY" ) ;
729+ }
730+ assert_eq ! ( key, "env-fallback-key" ) ;
731+ }
732+
733+ #[ test]
734+ fn load_api_key_blank_file_value_falls_back_to_environment ( ) {
735+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
736+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
737+ let env_file = tmp. path ( ) . join ( ".env" ) ;
738+ std:: fs:: write ( & env_file, "ZAI_API_KEY= \n " ) . unwrap ( ) ;
739+ unsafe {
740+ std:: env:: set_var ( "ZAI_API_KEY" , "env-fallback-key" ) ;
741+ }
742+ let key = load_api_key ( & env_file) . unwrap ( ) ;
743+ unsafe {
744+ std:: env:: remove_var ( "ZAI_API_KEY" ) ;
745+ }
746+ assert_eq ! ( key, "env-fallback-key" ) ;
747+ }
748+
749+ #[ test]
750+ fn load_api_key_duplicate_entries_last_wins ( ) {
751+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
752+ unsafe {
753+ std:: env:: remove_var ( "ZAI_API_KEY" ) ;
754+ }
755+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
756+ let env_file = tmp. path ( ) . join ( ".env" ) ;
757+ std:: fs:: write ( & env_file, "ZAI_API_KEY=first\n ZAI_API_KEY=second\n " ) . unwrap ( ) ;
758+ let key = load_api_key ( & env_file) . unwrap ( ) ;
759+ assert_eq ! ( key, "second" ) ;
760+ }
761+
762+ #[ test]
763+ fn load_api_key_export_statement_is_parsed ( ) {
764+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
765+ unsafe {
766+ std:: env:: remove_var ( "ZAI_API_KEY" ) ;
767+ }
768+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
769+ let env_file = tmp. path ( ) . join ( ".env" ) ;
770+ std:: fs:: write ( & env_file, "export ZAI_API_KEY=from-export\n " ) . unwrap ( ) ;
771+ let key = load_api_key ( & env_file) . unwrap ( ) ;
772+ assert_eq ! ( key, "from-export" ) ;
773+ }
774+
775+ #[ test]
776+ fn load_api_key_interpolation_follows_dotenvy_behavior ( ) {
777+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
778+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
779+ let env_file = tmp. path ( ) . join ( ".env" ) ;
780+ unsafe {
781+ std:: env:: set_var ( "BASE_KEY" , "root" ) ;
782+ std:: env:: remove_var ( "ZAI_API_KEY" ) ;
783+ }
784+ std:: fs:: write ( & env_file, "ZAI_API_KEY=${BASE_KEY}-suffix\n " ) . unwrap ( ) ;
785+ let key = load_api_key ( & env_file) . unwrap ( ) ;
786+ unsafe {
787+ std:: env:: remove_var ( "BASE_KEY" ) ;
788+ }
789+ assert_eq ! ( key, "root-suffix" ) ;
790+ }
791+
792+ #[ test]
793+ fn load_api_key_invalid_file_is_hard_error ( ) {
794+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
795+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
796+ let env_file = tmp. path ( ) . join ( ".env" ) ;
797+ unsafe {
798+ std:: env:: set_var ( "ZAI_API_KEY" , "env-fallback-key" ) ;
799+ }
800+ std:: fs:: write ( & env_file, "ZAI_API_KEY='unterminated\n " ) . unwrap ( ) ;
801+ let err = load_api_key ( & env_file) . unwrap_err ( ) . to_string ( ) ;
802+ unsafe {
803+ std:: env:: remove_var ( "ZAI_API_KEY" ) ;
804+ }
805+ assert ! ( err. contains( "Failed to read or parse env file" ) ) ;
806+ }
807+
808+ #[ cfg( unix) ]
809+ #[ test]
810+ fn load_api_key_unreadable_file_does_not_fall_back_to_environment ( ) {
811+ use std:: os:: unix:: fs:: PermissionsExt ;
812+
813+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
814+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
815+ let locked_dir = tmp. path ( ) . join ( "locked" ) ;
816+ std:: fs:: create_dir_all ( & locked_dir) . unwrap ( ) ;
817+ let env_file = locked_dir. join ( ".env" ) ;
818+ std:: fs:: write ( & env_file, "ZAI_API_KEY=file-key\n " ) . unwrap ( ) ;
819+ unsafe {
820+ std:: env:: set_var ( "ZAI_API_KEY" , "env-fallback-key" ) ;
821+ }
822+ std:: fs:: set_permissions ( & locked_dir, std:: fs:: Permissions :: from_mode ( 0o000 ) ) . unwrap ( ) ;
823+ let err = load_api_key ( & env_file) . unwrap_err ( ) . to_string ( ) ;
824+ std:: fs:: set_permissions ( & locked_dir, std:: fs:: Permissions :: from_mode ( 0o755 ) ) . unwrap ( ) ;
825+ unsafe {
826+ std:: env:: remove_var ( "ZAI_API_KEY" ) ;
827+ }
828+ assert ! ( err. contains( "Failed to access env file metadata" ) ) ;
829+ }
830+
704831 #[ test]
705832 fn validate_layout_response_success_with_usage ( ) {
706833 let ( md, details, usage) = validate_layout_response ( json ! ( {
@@ -1103,7 +1230,7 @@ mod tests {
11031230 }
11041231
11051232 #[ test]
1106- fn load_api_key_prefers_environment_variable ( ) {
1233+ fn load_api_key_prefers_env_file_over_environment_variable ( ) {
11071234 let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
11081235 let tmp = TempDir :: new ( ) . unwrap ( ) ;
11091236 let env_file = tmp. path ( ) . join ( ".env" ) ;
@@ -1117,7 +1244,7 @@ mod tests {
11171244 std:: env:: remove_var ( "ZAI_API_KEY" ) ;
11181245 }
11191246
1120- assert_eq ! ( key, "env -key" ) ;
1247+ assert_eq ! ( key, "file -key" ) ;
11211248 }
11221249
11231250 #[ test]
0 commit comments