-
Notifications
You must be signed in to change notification settings - Fork 127
Use path-component semantics for crawl controls #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -204,7 +204,7 @@ func Key(u *url.URL) string { return u.String() } | |
| type ScopeConfig struct { | ||
| IncludeSubdomains bool | ||
| ScopePrefix string // only crawl paths under this prefix, e.g. "/docs/" | ||
| ExcludePaths []string // skip any path containing one of these substrings | ||
| ExcludePaths []string // skip any path that equals or is under one of these prefixes | ||
| } | ||
|
|
||
| // SameSite reports whether u belongs to the seed's site: the same host, or a | ||
|
|
@@ -253,17 +253,40 @@ func InScope(seed, u *url.URL, cfg ScopeConfig) bool { | |
| if !SameSite(seed, u, cfg.IncludeSubdomains) { | ||
| return false | ||
| } | ||
| if cfg.ScopePrefix != "" && !strings.HasPrefix(u.Path, cfg.ScopePrefix) { | ||
| if cfg.ScopePrefix != "" && !pathHasPrefix(u.Path, cfg.ScopePrefix) { | ||
| return false | ||
| } | ||
| for _, ex := range cfg.ExcludePaths { | ||
| if ex != "" && strings.Contains(u.Path, ex) { | ||
| if ex != "" && pathHasPrefix(u.Path, ex) { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // pathHasPrefix reports whether path equals prefix or is a descendant of it. | ||
| // Both sides are treated as URL paths: a prefix of "/api" matches "/api", | ||
| // "/api/", and "/api/v1", but not "/apiv1" or "/map/api". A prefix without a | ||
| // leading slash is normalised to one so CLI prefixes such as "api" behave like | ||
| // "/api" for both --scope-prefix and --exclude. | ||
| func pathHasPrefix(path, prefix string) bool { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the right implementation. Exact match, tolerant of a trailing slash on either side, and requiring a One gap: the leading-slash normalisation on the next few lines (so |
||
| if prefix == "" { | ||
| return false | ||
| } | ||
| if !strings.HasPrefix(prefix, "/") { | ||
| prefix = "/" + prefix | ||
| } | ||
| // Exact match, or prefix followed by '/' so "/api" does not match "/apiv1". | ||
| if path == prefix || path == strings.TrimSuffix(prefix, "/") { | ||
| return true | ||
| } | ||
| p := prefix | ||
| if !strings.HasSuffix(p, "/") { | ||
| p += "/" | ||
| } | ||
| return strings.HasPrefix(path, p) | ||
| } | ||
|
|
||
| // LikelyPage reports whether an <a href> target should be rendered as a page | ||
| // rather than downloaded as a file. Links ending in a known binary/document | ||
| // extension are treated as assets. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,9 +103,18 @@ func TestInScope(t *testing.T) { | |
| {"https://other.com/a", ScopeConfig{}, false}, | ||
| {"https://sub.ex.com/a", ScopeConfig{}, false}, | ||
| {"https://sub.ex.com/a", ScopeConfig{IncludeSubdomains: true}, true}, | ||
| {"https://ex.com/docs", ScopeConfig{ScopePrefix: "/docs/"}, true}, | ||
| {"https://ex.com/docs/x", ScopeConfig{ScopePrefix: "/docs/"}, true}, | ||
| {"https://ex.com/docs/x", ScopeConfig{ScopePrefix: "docs"}, true}, | ||
| {"https://ex.com/documentation", ScopeConfig{ScopePrefix: "/docs"}, false}, | ||
| {"https://ex.com/blog/x", ScopeConfig{ScopePrefix: "/docs/"}, false}, | ||
| {"https://ex.com/a/private/x", ScopeConfig{ExcludePaths: []string{"/private/"}}, false}, | ||
| // Exclude is a path prefix, not a substring: /private matches /private | ||
| // and /private/x, but not /a/private/x or /privatething. | ||
| {"https://ex.com/private/x", ScopeConfig{ExcludePaths: []string{"/private"}}, false}, | ||
| {"https://ex.com/private", ScopeConfig{ExcludePaths: []string{"/private"}}, false}, | ||
| {"https://ex.com/private/x", ScopeConfig{ExcludePaths: []string{"private"}}, false}, | ||
| {"https://ex.com/a/private/x", ScopeConfig{ExcludePaths: []string{"/private"}}, true}, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is the proof that the change is not just a bug fix. You are flipping an existing assertion from That is fine by me, the new behaviour is what the docs always promised, but it needs to be announced as a behaviour change rather than buried in Fixed. |
||
| {"https://ex.com/privatething", ScopeConfig{ExcludePaths: []string{"/private"}}, true}, | ||
| {"https://ex.com/a/public/x", ScopeConfig{ExcludePaths: []string{"/private/"}}, true}, | ||
| } | ||
| for _, c := range cases { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you are in this function,
ScopePrefixa few lines above still uses a plainstrings.HasPrefix(u.Path, cfg.ScopePrefix), so--scope-prefix /docalso matches/documentation/. That is the same class of bug you are fixing here.Leaving one half of
InScopeon path-component semantics and the other half on string-prefix semantics is worse than fixing neither, because nobody can reason about the function without reading it. Please runpathHasPrefixon both.