@@ -13,8 +13,9 @@ use headers::{ContentLength, ContentType, HeaderMapExt};
13
13
use humansize:: FormatSize ;
14
14
use hyper:: { Body , Method , Response , StatusCode } ;
15
15
use mime_guess:: mime;
16
- use percent_encoding:: { percent_decode_str, utf8_percent_encode , AsciiSet , NON_ALPHANUMERIC } ;
16
+ use percent_encoding:: { percent_decode_str, percent_encode , AsciiSet , NON_ALPHANUMERIC } ;
17
17
use serde:: { Serialize , Serializer } ;
18
+ use std:: ffi:: { OsStr , OsString } ;
18
19
use std:: future:: Future ;
19
20
use std:: io;
20
21
use std:: path:: Path ;
@@ -152,16 +153,15 @@ enum FileType {
152
153
/// Defines a file entry and its properties.
153
154
#[ derive( Serialize ) ]
154
155
struct FileEntry {
155
- name : String ,
156
- #[ serde( skip_serializing) ]
157
- name_encoded : String ,
156
+ #[ serde( serialize_with = "serialize_name" ) ]
157
+ name : OsString ,
158
158
#[ serde( serialize_with = "serialize_mtime" ) ]
159
159
mtime : Option < DateTime < Local > > ,
160
160
#[ serde( skip_serializing_if = "Option::is_none" ) ]
161
161
size : Option < u64 > ,
162
162
r#type : FileType ,
163
163
#[ serde( skip_serializing) ]
164
- uri : Option < String > ,
164
+ uri : String ,
165
165
}
166
166
167
167
impl FileEntry {
@@ -205,36 +205,19 @@ fn read_dir_entries(
205
205
}
206
206
} ;
207
207
208
- // FIXME: handle non-Unicode file names properly via OsString
209
- let name = match dir_entry
210
- . file_name ( )
211
- . into_string ( )
212
- . map_err ( |err| anyhow:: anyhow!( err. into_string( ) . unwrap_or_default( ) ) )
213
- {
214
- Ok ( s) => s,
215
- Err ( err) => {
216
- tracing:: error!(
217
- "unable to resolve name for file or directory entry (skipped): {:?}" ,
218
- err
219
- ) ;
220
- continue ;
221
- }
222
- } ;
208
+ let name = dir_entry. file_name ( ) ;
223
209
224
210
// Check and ignore the current hidden file/directory (dotfile) if feature enabled
225
- if ignore_hidden_files && name. starts_with ( '.' ) {
211
+ if ignore_hidden_files && name. as_encoded_bytes ( ) . first ( ) . is_some_and ( |c| * c == b '.') {
226
212
continue ;
227
213
}
228
214
229
- let mut name_encoded = utf8_percent_encode ( & name, PERCENT_ENCODE_SET ) . to_string ( ) ;
230
- let mut size = None ;
231
-
232
- if meta. is_dir ( ) {
233
- name_encoded. push ( '/' ) ;
215
+ let ( r#type, size) = if meta. is_dir ( ) {
234
216
dirs_count += 1 ;
217
+ ( FileType :: Directory , None )
235
218
} else if meta. is_file ( ) {
236
- size = Some ( meta. len ( ) ) ;
237
219
files_count += 1 ;
220
+ ( FileType :: File , Some ( meta. len ( ) ) )
238
221
} else if meta. file_type ( ) . is_symlink ( ) {
239
222
// NOTE: we resolve the symlink path below to just know if is a directory or not.
240
223
// Hwever, we are still showing the symlink name but not the resolved name.
@@ -246,7 +229,7 @@ fn read_dir_entries(
246
229
tracing:: error!(
247
230
"unable to resolve `{}` symlink path (skipped): {:?}" ,
248
231
symlink. display( ) ,
249
- err
232
+ err,
250
233
) ;
251
234
continue ;
252
235
}
@@ -258,59 +241,43 @@ fn read_dir_entries(
258
241
tracing:: error!(
259
242
"unable to resolve metadata for `{}` symlink (skipped): {:?}" ,
260
243
symlink. display( ) ,
261
- err
244
+ err,
262
245
) ;
263
246
continue ;
264
247
}
265
248
} ;
266
249
if symlink_meta. is_dir ( ) {
267
- name_encoded. push ( '/' ) ;
268
250
dirs_count += 1 ;
251
+ ( FileType :: Directory , None )
269
252
} else {
270
- size = Some ( meta. len ( ) ) ;
271
253
files_count += 1 ;
254
+ ( FileType :: File , Some ( symlink_meta. len ( ) ) )
272
255
}
273
256
} else {
274
257
continue ;
275
- }
258
+ } ;
259
+
260
+ let name_encoded = percent_encode ( name. as_encoded_bytes ( ) , PERCENT_ENCODE_SET ) . to_string ( ) ;
276
261
277
- let mut uri = None ;
278
262
// NOTE: Use relative paths by default independently of
279
263
// the "redirect trailing slash" feature.
280
264
// However, when "redirect trailing slash" is disabled
281
265
// and a request path doesn't contain a trailing slash then
282
266
// entries should contain the "parent/entry-name" as a link format.
283
267
// Otherwise, we just use the "entry-name" as a link (default behavior).
284
268
// Note that in both cases, we add a trailing slash if the entry is a directory.
285
- if !base_path. ends_with ( '/' ) {
286
- let base_path = Path :: new ( base_path) ;
287
- let parent_dir = base_path. parent ( ) . unwrap_or ( base_path) ;
288
- let mut base_dir = base_path;
289
- if base_path != parent_dir {
290
- base_dir = match base_path. strip_prefix ( parent_dir) {
291
- Ok ( v) => v,
292
- Err ( err) => {
293
- tracing:: error!(
294
- "unable to strip parent path prefix for `{}` (skipped): {:?}" ,
295
- base_path. display( ) ,
296
- err
297
- ) ;
298
- continue ;
299
- }
300
- } ;
301
- }
302
-
303
- let mut base_str = String :: new ( ) ;
304
- if !base_dir. starts_with ( "/" ) {
305
- let base_dir = base_dir. to_str ( ) . unwrap_or_default ( ) ;
306
- if !base_dir. is_empty ( ) {
307
- base_str. push_str ( base_dir) ;
308
- }
309
- base_str. push ( '/' ) ;
310
- }
269
+ let mut uri = if !base_path. ends_with ( '/' ) && !base_path. is_empty ( ) {
270
+ let parent = base_path
271
+ . rsplit_once ( '/' )
272
+ . map ( |( _, parent) | parent)
273
+ . unwrap_or ( base_path) ;
274
+ format ! ( "{parent}/{name_encoded}" )
275
+ } else {
276
+ name_encoded
277
+ } ;
311
278
312
- base_str . push_str ( & name_encoded ) ;
313
- uri = Some ( base_str ) ;
279
+ if r#type == FileType :: Directory {
280
+ uri. push ( '/' ) ;
314
281
}
315
282
316
283
let mtime = match parse_last_modified ( meta. modified ( ) ?) {
@@ -320,15 +287,9 @@ fn read_dir_entries(
320
287
None
321
288
}
322
289
} ;
323
- let r#type = if meta. is_dir ( ) {
324
- FileType :: Directory
325
- } else {
326
- FileType :: File
327
- } ;
328
290
329
291
file_entries. push ( FileEntry {
330
292
name,
331
- name_encoded,
332
293
mtime,
333
294
size,
334
295
r#type,
@@ -379,7 +340,7 @@ fn read_dir_entries(
379
340
files_count,
380
341
& mut file_entries,
381
342
order_code,
382
- ) ?
343
+ )
383
344
}
384
345
} ;
385
346
@@ -403,6 +364,11 @@ fn json_auto_index(entries: &mut [FileEntry], order_code: u8) -> Result<String>
403
364
Ok ( serde_json:: to_string ( entries) ?)
404
365
}
405
366
367
+ /// Serialize FileEntry::name
368
+ fn serialize_name < S : Serializer > ( name : & OsStr , serializer : S ) -> Result < S :: Ok , S :: Error > {
369
+ serializer. serialize_str ( & name. to_string_lossy ( ) )
370
+ }
371
+
406
372
/// Serialize FileEntry::mtime field
407
373
fn serialize_mtime < S : Serializer > (
408
374
mtime : & Option < DateTime < Local > > ,
@@ -425,13 +391,13 @@ fn html_auto_index<'a>(
425
391
files_count : usize ,
426
392
entries : & ' a mut [ FileEntry ] ,
427
393
order_code : u8 ,
428
- ) -> Result < String > {
394
+ ) -> String {
429
395
use maud:: { html, DOCTYPE } ;
430
396
431
397
let sort_attrs = sort_file_entries ( entries, order_code) ;
432
- let current_path = percent_decode_str ( base_path) . decode_utf8 ( ) ? . to_string ( ) ;
398
+ let current_path = percent_decode_str ( base_path) . decode_utf8_lossy ( ) ;
433
399
434
- Ok ( html ! {
400
+ html ! {
435
401
( DOCTYPE )
436
402
html {
437
403
head {
@@ -490,8 +456,8 @@ fn html_auto_index<'a>(
490
456
@for entry in entries {
491
457
tr {
492
458
td {
493
- a href=( entry. uri. as_ref ( ) . unwrap_or ( & entry . name_encoded ) ) {
494
- ( entry. name)
459
+ a href=( entry. uri) {
460
+ ( entry. name. to_string_lossy ( ) )
495
461
@if entry. is_dir( ) {
496
462
"/"
497
463
}
@@ -519,7 +485,7 @@ fn html_auto_index<'a>(
519
485
}
520
486
}
521
487
}
522
- } . into ( ) )
488
+ } . into ( )
523
489
}
524
490
525
491
/// Sort a list of file entries by a specific order code.
@@ -532,7 +498,7 @@ fn sort_file_entries(files: &mut [FileEntry], order_code: u8) -> SortingAttr<'_>
532
498
match order_code {
533
499
0 | 1 => {
534
500
// Name (asc, desc)
535
- files. sort_by_cached_key ( |f| f. name . to_lowercase ( ) ) ;
501
+ files. sort_by_cached_key ( |f| f. name . to_string_lossy ( ) . to_lowercase ( ) ) ;
536
502
if order_code == 1 {
537
503
files. reverse ( ) ;
538
504
} else {
0 commit comments