@@ -7,12 +7,16 @@ use super::ExtendedBigDecimal;
7
7
use crate :: format:: spec:: ArgumentLocation ;
8
8
use crate :: {
9
9
error:: set_exit_code,
10
+ os_str_as_bytes,
10
11
parser:: num_parser:: { ExtendedParser , ExtendedParserError } ,
11
12
quoting_style:: { QuotingStyle , locale_aware_escape_name} ,
12
13
show_error, show_warning,
13
14
} ;
14
15
use os_display:: Quotable ;
15
- use std:: { ffi:: OsStr , num:: NonZero } ;
16
+ use std:: {
17
+ ffi:: { OsStr , OsString } ,
18
+ num:: NonZero ,
19
+ } ;
16
20
17
21
/// An argument for formatting
18
22
///
@@ -24,12 +28,12 @@ use std::{ffi::OsStr, num::NonZero};
24
28
#[ derive( Clone , Debug , PartialEq ) ]
25
29
pub enum FormatArgument {
26
30
Char ( char ) ,
27
- String ( String ) ,
31
+ String ( OsString ) ,
28
32
UnsignedInt ( u64 ) ,
29
33
SignedInt ( i64 ) ,
30
34
Float ( ExtendedBigDecimal ) ,
31
35
/// Special argument that gets coerced into the other variants
32
- Unparsed ( String ) ,
36
+ Unparsed ( OsString ) ,
33
37
}
34
38
35
39
/// A struct that holds a slice of format arguments and provides methods to access them
@@ -72,62 +76,115 @@ impl<'a> FormatArguments<'a> {
72
76
pub fn next_char ( & mut self , position : & ArgumentLocation ) -> u8 {
73
77
match self . next_arg ( position) {
74
78
Some ( FormatArgument :: Char ( c) ) => * c as u8 ,
75
- Some ( FormatArgument :: Unparsed ( s) ) => s. bytes ( ) . next ( ) . unwrap_or ( b'\0' ) ,
79
+ Some ( FormatArgument :: Unparsed ( os) ) => match os_str_as_bytes ( os) {
80
+ Ok ( bytes) => bytes. first ( ) . copied ( ) . unwrap_or ( b'\0' ) ,
81
+ Err ( _) => b'\0' ,
82
+ } ,
76
83
_ => b'\0' ,
77
84
}
78
85
}
79
86
80
- pub fn next_string ( & mut self , position : & ArgumentLocation ) -> & ' a str {
87
+ pub fn next_string ( & mut self , position : & ArgumentLocation ) -> & ' a OsStr {
81
88
match self . next_arg ( position) {
82
- Some ( FormatArgument :: Unparsed ( s ) | FormatArgument :: String ( s ) ) => s ,
83
- _ => "" ,
89
+ Some ( FormatArgument :: Unparsed ( os ) | FormatArgument :: String ( os ) ) => os ,
90
+ _ => "" . as_ref ( ) ,
84
91
}
85
92
}
86
93
87
94
pub fn next_i64 ( & mut self , position : & ArgumentLocation ) -> i64 {
88
95
match self . next_arg ( position) {
89
96
Some ( FormatArgument :: SignedInt ( n) ) => * n,
90
- Some ( FormatArgument :: Unparsed ( s ) ) => extract_value ( i64 :: extended_parse ( s ) , s ) ,
97
+ Some ( FormatArgument :: Unparsed ( os ) ) => Self :: get_num :: < i64 > ( os ) ,
91
98
_ => 0 ,
92
99
}
93
100
}
94
101
95
102
pub fn next_u64 ( & mut self , position : & ArgumentLocation ) -> u64 {
96
103
match self . next_arg ( position) {
97
104
Some ( FormatArgument :: UnsignedInt ( n) ) => * n,
98
- Some ( FormatArgument :: Unparsed ( s) ) => {
99
- // Check if the string is a character literal enclosed in quotes
100
- if s. starts_with ( [ '"' , '\'' ] ) {
101
- // Extract the content between the quotes safely using chars
102
- let mut chars = s. trim_matches ( |c| c == '"' || c == '\'' ) . chars ( ) ;
103
- if let Some ( first_char) = chars. next ( ) {
104
- if chars. clone ( ) . count ( ) > 0 {
105
- // Emit a warning if there are additional characters
106
- let remaining: String = chars. collect ( ) ;
107
- show_warning ! (
108
- "{remaining}: character(s) following character constant have been ignored"
109
- ) ;
110
- }
111
- return first_char as u64 ; // Use only the first character
112
- }
113
- return 0 ; // Empty quotes
114
- }
115
- extract_value ( u64:: extended_parse ( s) , s)
116
- }
105
+ Some ( FormatArgument :: Unparsed ( os) ) => Self :: get_num :: < u64 > ( os) ,
117
106
_ => 0 ,
118
107
}
119
108
}
120
109
121
110
pub fn next_extended_big_decimal ( & mut self , position : & ArgumentLocation ) -> ExtendedBigDecimal {
122
111
match self . next_arg ( position) {
123
112
Some ( FormatArgument :: Float ( n) ) => n. clone ( ) ,
124
- Some ( FormatArgument :: Unparsed ( s) ) => {
125
- extract_value ( ExtendedBigDecimal :: extended_parse ( s) , s)
126
- }
113
+ Some ( FormatArgument :: Unparsed ( os) ) => Self :: get_num :: < ExtendedBigDecimal > ( os) ,
127
114
_ => ExtendedBigDecimal :: zero ( ) ,
128
115
}
129
116
}
130
117
118
+ // Parse an OsStr that we know to start with a '/"
119
+ fn parse_quote_start < T > ( os : & OsStr ) -> Result < T , ExtendedParserError < T > >
120
+ where
121
+ T : ExtendedParser + From < u8 > + From < u32 > + Default ,
122
+ {
123
+ // If this fails (this can only happens on Windows), then just
124
+ // return NotNumeric.
125
+ let s = match os_str_as_bytes ( os) {
126
+ Ok ( s) => s,
127
+ Err ( _) => return Err ( ExtendedParserError :: NotNumeric ) ,
128
+ } ;
129
+
130
+ let bytes = match s. split_first ( ) {
131
+ Some ( ( b'"' , bytes) ) | Some ( ( b'\'' , bytes) ) => bytes,
132
+ _ => {
133
+ // This really can't happen, the string we are given must start with '/".
134
+ debug_assert ! ( false ) ;
135
+ return Err ( ExtendedParserError :: NotNumeric ) ;
136
+ }
137
+ } ;
138
+
139
+ if bytes. is_empty ( ) {
140
+ return Err ( ExtendedParserError :: NotNumeric ) ;
141
+ }
142
+
143
+ let ( val, len) = if let Some ( c) = bytes
144
+ . utf8_chunks ( )
145
+ . next ( )
146
+ . expect ( "bytes should not be empty" )
147
+ . valid ( )
148
+ . chars ( )
149
+ . next ( )
150
+ {
151
+ // Valid UTF-8 character, cast the codepoint to u32 then T
152
+ // (largest unicode codepoint is only 3 bytes, so this is safe)
153
+ ( ( c as u32 ) . into ( ) , c. len_utf8 ( ) )
154
+ } else {
155
+ // Not a valid UTF-8 character, use the first byte
156
+ ( bytes[ 0 ] . into ( ) , 1 )
157
+ } ;
158
+ // Emit a warning if there are additional characters
159
+ if bytes. len ( ) > len {
160
+ return Err ( ExtendedParserError :: PartialMatch (
161
+ val,
162
+ String :: from_utf8_lossy ( & bytes[ len..] ) . into_owned ( ) ,
163
+ ) ) ;
164
+ }
165
+
166
+ Ok ( val)
167
+ }
168
+
169
+ fn get_num < T > ( os : & OsStr ) -> T
170
+ where
171
+ T : ExtendedParser + From < u8 > + From < u32 > + Default ,
172
+ {
173
+ let s = os. to_string_lossy ( ) ;
174
+ let first = s. as_bytes ( ) . first ( ) . copied ( ) ;
175
+
176
+ let quote_start = first == Some ( b'"' ) || first == Some ( b'\'' ) ;
177
+ let parsed = if quote_start {
178
+ // The string begins with a quote
179
+ Self :: parse_quote_start ( os)
180
+ } else {
181
+ T :: extended_parse ( & s)
182
+ } ;
183
+
184
+ // Get the best possible value, even if parsed was an error.
185
+ extract_value ( parsed, & s, quote_start)
186
+ }
187
+
131
188
fn get_at_relative_position ( & mut self , pos : NonZero < usize > ) -> Option < & ' a FormatArgument > {
132
189
let pos: usize = pos. into ( ) ;
133
190
let pos = ( pos - 1 ) . saturating_add ( self . current_offset ) ;
@@ -147,7 +204,11 @@ impl<'a> FormatArguments<'a> {
147
204
}
148
205
}
149
206
150
- fn extract_value < T : Default > ( p : Result < T , ExtendedParserError < ' _ , T > > , input : & str ) -> T {
207
+ fn extract_value < T : Default > (
208
+ p : Result < T , ExtendedParserError < T > > ,
209
+ input : & str ,
210
+ quote_start : bool ,
211
+ ) -> T {
151
212
match p {
152
213
Ok ( v) => v,
153
214
Err ( e) => {
@@ -167,14 +228,15 @@ fn extract_value<T: Default>(p: Result<T, ExtendedParserError<'_, T>>, input: &s
167
228
Default :: default ( )
168
229
}
169
230
ExtendedParserError :: PartialMatch ( v, rest) => {
170
- let bytes = input . as_encoded_bytes ( ) ;
171
- if !bytes . is_empty ( ) && ( bytes [ 0 ] == b'\'' || bytes [ 0 ] == b'"' ) {
231
+ if quote_start {
232
+ set_exit_code ( 0 ) ;
172
233
show_warning ! (
173
234
"{rest}: character(s) following character constant have been ignored"
174
235
) ;
175
236
} else {
176
237
show_error ! ( "{}: value not completely converted" , input. quote( ) ) ;
177
238
}
239
+
178
240
v
179
241
}
180
242
}
@@ -249,11 +311,11 @@ mod tests {
249
311
// Test with different method types in sequence
250
312
let args = [
251
313
FormatArgument :: Char ( 'a' ) ,
252
- FormatArgument :: String ( "hello" . to_string ( ) ) ,
253
- FormatArgument :: Unparsed ( "123" . to_string ( ) ) ,
254
- FormatArgument :: String ( "world" . to_string ( ) ) ,
314
+ FormatArgument :: String ( "hello" . into ( ) ) ,
315
+ FormatArgument :: Unparsed ( "123" . into ( ) ) ,
316
+ FormatArgument :: String ( "world" . into ( ) ) ,
255
317
FormatArgument :: Char ( 'z' ) ,
256
- FormatArgument :: String ( "test" . to_string ( ) ) ,
318
+ FormatArgument :: String ( "test" . into ( ) ) ,
257
319
] ;
258
320
let mut args = FormatArguments :: new ( & args) ;
259
321
@@ -384,10 +446,10 @@ mod tests {
384
446
fn test_unparsed_arguments ( ) {
385
447
// Test with unparsed arguments that get coerced
386
448
let args = [
387
- FormatArgument :: Unparsed ( "hello" . to_string ( ) ) ,
388
- FormatArgument :: Unparsed ( "123" . to_string ( ) ) ,
389
- FormatArgument :: Unparsed ( "hello" . to_string ( ) ) ,
390
- FormatArgument :: Unparsed ( "456" . to_string ( ) ) ,
449
+ FormatArgument :: Unparsed ( "hello" . into ( ) ) ,
450
+ FormatArgument :: Unparsed ( "123" . into ( ) ) ,
451
+ FormatArgument :: Unparsed ( "hello" . into ( ) ) ,
452
+ FormatArgument :: Unparsed ( "456" . into ( ) ) ,
391
453
] ;
392
454
let mut args = FormatArguments :: new ( & args) ;
393
455
@@ -409,10 +471,10 @@ mod tests {
409
471
// Test with mixed types and positional access
410
472
let args = [
411
473
FormatArgument :: Char ( 'a' ) ,
412
- FormatArgument :: String ( "test" . to_string ( ) ) ,
474
+ FormatArgument :: String ( "test" . into ( ) ) ,
413
475
FormatArgument :: UnsignedInt ( 42 ) ,
414
476
FormatArgument :: Char ( 'b' ) ,
415
- FormatArgument :: String ( "more" . to_string ( ) ) ,
477
+ FormatArgument :: String ( "more" . into ( ) ) ,
416
478
FormatArgument :: UnsignedInt ( 99 ) ,
417
479
] ;
418
480
let mut args = FormatArguments :: new ( & args) ;
0 commit comments