InferFilenameFromURL is the final naming fallback when neither the user nor the probe produced a trustworthy filename.
(rawURL string)
| 17 | // InferFilenameFromURL is the final naming fallback when neither the user nor |
| 18 | // the probe produced a trustworthy filename. |
| 19 | func InferFilenameFromURL(rawURL string) string { |
| 20 | parsed, err := url.Parse(strings.TrimSpace(rawURL)) |
| 21 | if err != nil { |
| 22 | return "" |
| 23 | } |
| 24 | |
| 25 | isSafeComponent := func(name string) bool { |
| 26 | name = strings.TrimSpace(name) |
| 27 | if name == "" { |
| 28 | return false |
| 29 | } |
| 30 | // Keep URL-derived names inside the chosen destination directory. |
| 31 | if strings.Contains(name, "/") || strings.Contains(name, "\\") { |
| 32 | return false |
| 33 | } |
| 34 | if name == "." || name == ".." || name == "/" { |
| 35 | return false |
| 36 | } |
| 37 | // Reject simple Windows absolute paths like C:foo or C:\foo |
| 38 | if len(name) >= 2 && (name[1] == ':' && ((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z'))) { |
| 39 | return false |
| 40 | } |
| 41 | return true |
| 42 | } |
| 43 | |
| 44 | query := parsed.Query() |
| 45 | if name := strings.TrimSpace(query.Get("filename")); name != "" { |
| 46 | if base := strings.TrimSpace(path.Base(name)); isSafeComponent(base) { |
| 47 | utils.Debug("Inferred filename from query param 'filename': %s", base) |
| 48 | return base |
| 49 | } |
| 50 | } |
| 51 | if name := strings.TrimSpace(query.Get("file")); name != "" { |
| 52 | if base := strings.TrimSpace(path.Base(name)); isSafeComponent(base) { |
| 53 | utils.Debug("Inferred filename from query param 'file': %s", base) |
| 54 | return base |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | base := strings.TrimSpace(path.Base(parsed.Path)) |
| 59 | if !isSafeComponent(base) { |
| 60 | return "" |
| 61 | } |
| 62 | utils.Debug("Inferred filename from URL path: %s", base) |
| 63 | return base |
| 64 | } |
| 65 | |
| 66 | // GetUniqueFilename keeps final files and .surge working files in the same |
| 67 | // collision namespace so concurrent or resumed downloads do not share a path. |