Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 51 additions & 17 deletions Sources/ExFig/Subcommands/DownloadImageProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,63 @@ enum DownloadImageProcessor {
) -> String {
var result = name

// Apply regex replacement if both patterns are specified
if let validateRegexp, let replaceRegexp {
// Normalize path separators first
result = result.replacingOccurrences(of: "/", with: "_")
// Apply regex replacement
if let regex = try? NSRegularExpression(pattern: validateRegexp) {
let range = NSRange(result.startIndex..., in: result)
result = regex.stringByReplacingMatches(
in: result,
range: range,
withTemplate: replaceRegexp
)
}
} else {
// Just normalize path separators
result = result.replacingOccurrences(of: "/", with: "_")
// 1. Initial sanitization: separators
result = result.replacingOccurrences(of: "/", with: "_")
result = result.replacingOccurrences(of: "\\", with: "_")

// 2. Apply regex replacement if both patterns are specified
if let validateRegexp, let replaceRegexp,
let regex = try? NSRegularExpression(pattern: validateRegexp)
{
let range = NSRange(result.startIndex..., in: result)
result = regex.stringByReplacingMatches(
in: result,
range: range,
withTemplate: replaceRegexp
)
}

// Apply name style
// 3. Apply name style
if let nameStyle {
result = applyNameStyle(result, style: nameStyle)
}

// 4. Final strict sanitization
// This ensures that even after regex/style application, the result is safe
result = sanitizeFilename(result)

return result
}

/// Strictly sanitizes a filename to prevent path traversal and ensure filesystem safety.
///
/// - Parameter name: The name to sanitize
/// - Returns: Sanitized name safe for use as a filename
private static func sanitizeFilename(_ name: String) -> String {
var result = name

// Replace any remaining path separators (just in case regex introduced them)
result = result.replacingOccurrences(of: "/", with: "_")
result = result.replacingOccurrences(of: "\\", with: "_")

// Prevent path traversal
result = result.replacingOccurrences(of: "..", with: "__")

// Remove control characters and other dangerous chars
// Reserved chars: : * ? " < > | (Windows) + control chars
let illegalChars = CharacterSet(charactersIn: ":*?\"<>|")
.union(.controlCharacters)
.union(.newlines)
.union(.illegalCharacters)

let components = result.components(separatedBy: illegalChars)
result = components.joined(separator: "_")

// Ensure non-empty result (fallback to "unnamed")
if result.isEmpty || result == "." || result == "_" {
return "unnamed"
}

return result
}

Expand Down
4 changes: 4 additions & 0 deletions Sources/ExFigCore/Processor/AssetsProcessor.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// swiftlint:disable file_length
import Foundation

/// Process asset name
public protocol AssetNameProcessable {
var nameReplaceRegexp: String? { get }
Expand Down Expand Up @@ -114,6 +116,7 @@ public struct ImagesProcessor: AssetsProcessable, Sendable {
result = String(split[0])
} else {
result = result.replacingOccurrences(of: "/", with: "_")
result = result.replacingOccurrences(of: "\\", with: "_")
}

// Apply nameReplaceRegexp if configured
Expand Down Expand Up @@ -460,6 +463,7 @@ public extension AssetsProcessable {
renamedAsset.name = String(split[0])
} else {
renamedAsset.name = renamedAsset.name.replacingOccurrences(of: "/", with: "_")
renamedAsset.name = renamedAsset.name.replacingOccurrences(of: "\\", with: "_")
}
return renamedAsset
}
Expand Down
Loading