17
17
//! should catch the majority of "broken link" cases.
18
18
19
19
use std:: cell:: { Cell , RefCell } ;
20
+ use std:: collections:: hash_map:: Entry ;
20
21
use std:: collections:: { HashMap , HashSet } ;
21
22
use std:: fs;
22
- use std:: io :: ErrorKind ;
23
+ use std:: iter :: once ;
23
24
use std:: path:: { Component , Path , PathBuf } ;
24
25
use std:: rc:: Rc ;
25
26
use std:: time:: Instant ;
@@ -112,6 +113,7 @@ macro_rules! t {
112
113
113
114
struct Cli {
114
115
docs : PathBuf ,
116
+ link_targets_dirs : Vec < PathBuf > ,
115
117
}
116
118
117
119
fn main ( ) {
@@ -123,7 +125,11 @@ fn main() {
123
125
}
124
126
} ;
125
127
126
- let mut checker = Checker { root : cli. docs . clone ( ) , cache : HashMap :: new ( ) } ;
128
+ let mut checker = Checker {
129
+ root : cli. docs . clone ( ) ,
130
+ link_targets_dirs : cli. link_targets_dirs ,
131
+ cache : HashMap :: new ( ) ,
132
+ } ;
127
133
let mut report = Report {
128
134
errors : 0 ,
129
135
start : Instant :: now ( ) ,
@@ -144,19 +150,26 @@ fn main() {
144
150
}
145
151
146
152
fn parse_cli ( ) -> Result < Cli , String > {
147
- fn to_canonical_path ( arg : & str ) -> Result < PathBuf , String > {
148
- PathBuf :: from ( arg) . canonicalize ( ) . map_err ( |e| format ! ( "could not canonicalize {arg}: {e}" ) )
153
+ fn to_absolute_path ( arg : & str ) -> Result < PathBuf , String > {
154
+ std :: path :: absolute ( arg) . map_err ( |e| format ! ( "could not convert to absolute {arg}: {e}" ) )
149
155
}
150
156
151
157
let mut verbatim = false ;
152
158
let mut docs = None ;
159
+ let mut link_targets_dirs = Vec :: new ( ) ;
153
160
154
161
let mut args = std:: env:: args ( ) . skip ( 1 ) ;
155
162
while let Some ( arg) = args. next ( ) {
156
163
if !verbatim && arg == "--" {
157
164
verbatim = true ;
158
165
} else if !verbatim && ( arg == "-h" || arg == "--help" ) {
159
166
usage_and_exit ( 0 )
167
+ } else if !verbatim && arg == "--link-targets-dir" {
168
+ link_targets_dirs. push ( to_absolute_path (
169
+ & args. next ( ) . ok_or ( "missing value for --link-targets-dir" ) ?,
170
+ ) ?) ;
171
+ } else if !verbatim && let Some ( value) = arg. strip_prefix ( "--link-targets-dir=" ) {
172
+ link_targets_dirs. push ( to_absolute_path ( value) ?) ;
160
173
} else if !verbatim && arg. starts_with ( '-' ) {
161
174
return Err ( format ! ( "unknown flag: {arg}" ) ) ;
162
175
} else if docs. is_none ( ) {
@@ -166,16 +179,20 @@ fn parse_cli() -> Result<Cli, String> {
166
179
}
167
180
}
168
181
169
- Ok ( Cli { docs : to_canonical_path ( & docs. ok_or ( "missing first positional argument" ) ?) ? } )
182
+ Ok ( Cli {
183
+ docs : to_absolute_path ( & docs. ok_or ( "missing first positional argument" ) ?) ?,
184
+ link_targets_dirs,
185
+ } )
170
186
}
171
187
172
188
fn usage_and_exit ( code : i32 ) -> ! {
173
- eprintln ! ( "usage: linkchecker <path> " ) ;
189
+ eprintln ! ( "usage: linkchecker PATH [--link-targets-dir=PATH ...] " ) ;
174
190
std:: process:: exit ( code)
175
191
}
176
192
177
193
struct Checker {
178
194
root : PathBuf ,
195
+ link_targets_dirs : Vec < PathBuf > ,
179
196
cache : Cache ,
180
197
}
181
198
@@ -461,37 +478,34 @@ impl Checker {
461
478
462
479
/// Load a file from disk, or from the cache if available.
463
480
fn load_file ( & mut self , file : & Path , report : & mut Report ) -> ( String , & FileEntry ) {
464
- // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
465
- #[ cfg( windows) ]
466
- const ERROR_INVALID_NAME : i32 = 123 ;
467
-
468
481
let pretty_path =
469
482
file. strip_prefix ( & self . root ) . unwrap_or ( file) . to_str ( ) . unwrap ( ) . to_string ( ) ;
470
483
471
- let entry =
472
- self . cache . entry ( pretty_path. clone ( ) ) . or_insert_with ( || match fs:: metadata ( file) {
484
+ for base in once ( & self . root ) . chain ( self . link_targets_dirs . iter ( ) ) {
485
+ let entry = self . cache . entry ( pretty_path. clone ( ) ) ;
486
+ if let Entry :: Occupied ( e) = & entry
487
+ && !matches ! ( e. get( ) , FileEntry :: Missing )
488
+ {
489
+ break ;
490
+ }
491
+
492
+ let file = base. join ( & pretty_path) ;
493
+ entry. insert_entry ( match fs:: metadata ( & file) {
473
494
Ok ( metadata) if metadata. is_dir ( ) => FileEntry :: Dir ,
474
495
Ok ( _) => {
475
496
if file. extension ( ) . and_then ( |s| s. to_str ( ) ) != Some ( "html" ) {
476
497
FileEntry :: OtherFile
477
498
} else {
478
499
report. html_files += 1 ;
479
- load_html_file ( file, report)
500
+ load_html_file ( & file, report)
480
501
}
481
502
}
482
- Err ( e) if e. kind ( ) == ErrorKind :: NotFound => FileEntry :: Missing ,
483
- Err ( e) => {
484
- // If a broken intra-doc link contains `::`, on windows, it will cause `ERROR_INVALID_NAME` rather than `NotFound`.
485
- // Explicitly check for that so that the broken link can be allowed in `LINKCHECK_EXCEPTIONS`.
486
- #[ cfg( windows) ]
487
- if e. raw_os_error ( ) == Some ( ERROR_INVALID_NAME )
488
- && file. as_os_str ( ) . to_str ( ) . map_or ( false , |s| s. contains ( "::" ) )
489
- {
490
- return FileEntry :: Missing ;
491
- }
492
- panic ! ( "unexpected read error for {}: {}" , file. display( ) , e) ;
493
- }
503
+ Err ( e) if is_not_found_error ( & file, & e) => FileEntry :: Missing ,
504
+ Err ( e) => panic ! ( "unexpected read error for {}: {}" , file. display( ) , e) ,
494
505
} ) ;
506
+ }
507
+
508
+ let entry = self . cache . get ( & pretty_path) . unwrap ( ) ;
495
509
( pretty_path, entry)
496
510
}
497
511
}
@@ -670,3 +684,16 @@ fn parse_ids(ids: &mut HashSet<String>, file: &str, source: &str, report: &mut R
670
684
ids. insert ( encoded) ;
671
685
}
672
686
}
687
+
688
+ fn is_not_found_error ( path : & Path , error : & std:: io:: Error ) -> bool {
689
+ // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
690
+ const WINDOWS_ERROR_INVALID_NAME : i32 = 123 ;
691
+
692
+ error. kind ( ) == std:: io:: ErrorKind :: NotFound
693
+ // If a broken intra-doc link contains `::`, on windows, it will cause `ERROR_INVALID_NAME`
694
+ // rather than `NotFound`. Explicitly check for that so that the broken link can be allowed
695
+ // in `LINKCHECK_EXCEPTIONS`.
696
+ || ( cfg ! ( windows)
697
+ && error. raw_os_error ( ) == Some ( WINDOWS_ERROR_INVALID_NAME )
698
+ && path. as_os_str ( ) . to_str ( ) . map_or ( false , |s| s. contains ( "::" ) ) )
699
+ }
0 commit comments