Skip to content

Commit dcd96aa

Browse files
committed
wip
1 parent ccb171c commit dcd96aa

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

TablePro/Core/Database/SQLEscaping.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ enum SQLEscaping {
1616
/// Handles the following special characters:
1717
/// - Backslashes (must be escaped first to avoid double-escaping)
1818
/// - Single quotes (SQL standard: doubled)
19-
/// - Newlines, carriage returns, tabs, null bytes
19+
/// - Control characters: null, backspace, tab, newline, form feed, carriage return
20+
/// - MySQL EOF marker (\x1A) which can cause parsing issues
2021
///
2122
/// Example:
2223
/// ```swift
@@ -33,11 +34,15 @@ enum SQLEscaping {
3334
result = result.replacingOccurrences(of: "\\", with: "\\\\")
3435
// Single quote: SQL standard escaping (double the quote)
3536
result = result.replacingOccurrences(of: "'", with: "''")
36-
// Control characters
37+
// Common control characters
3738
result = result.replacingOccurrences(of: "\n", with: "\\n")
3839
result = result.replacingOccurrences(of: "\r", with: "\\r")
3940
result = result.replacingOccurrences(of: "\t", with: "\\t")
4041
result = result.replacingOccurrences(of: "\0", with: "\\0")
42+
// Additional control characters that can cause issues
43+
result = result.replacingOccurrences(of: "\u{08}", with: "\\b") // Backspace
44+
result = result.replacingOccurrences(of: "\u{0C}", with: "\\f") // Form feed
45+
result = result.replacingOccurrences(of: "\u{1A}", with: "\\Z") // MySQL EOF marker (Ctrl+Z)
4146
return result
4247
}
4348

TablePro/Core/Services/ExportService.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,17 @@ final class ExportService: ObservableObject {
493493
return result
494494
}
495495

496-
/// Format a value for JSON output
496+
/// Format a value for JSON output with optional type detection
497+
///
497498
/// - Parameters:
498499
/// - value: The value to format
499500
/// - preserveAsString: If true, always output as string without type detection
500501
/// (preserves leading zeros in ZIP codes, phone numbers, etc.)
502+
///
503+
/// - Note: When type detection is enabled (preserveAsString = false), integers beyond
504+
/// JavaScript's Number.MAX_SAFE_INTEGER (2^53-1 = 9007199254740991) may lose precision
505+
/// when parsed by JavaScript. For large IDs or precise numeric data, enable the
506+
/// "Preserve All Values as Strings" option in export settings.
501507
private func formatJSONValue(_ value: String?, preserveAsString: Bool) -> String {
502508
guard let val = value else { return "null" }
503509

@@ -507,6 +513,7 @@ final class ExportService: ObservableObject {
507513
}
508514

509515
// Try to detect numbers and booleans
516+
// Note: Large integers (> 2^53-1) may lose precision in JavaScript consumers
510517
if let intVal = Int(val) {
511518
return String(intVal)
512519
}
@@ -695,14 +702,23 @@ final class ExportService: ObservableObject {
695702
private func compressFileToFile(source: URL, destination: URL) async throws {
696703
// Run compression on background thread to avoid blocking main thread
697704
try await Task.detached(priority: .userInitiated) {
705+
// Pre-flight check: verify gzip is available
706+
let gzipPath = "/usr/bin/gzip"
707+
guard FileManager.default.isExecutableFile(atPath: gzipPath) else {
708+
throw ExportError.exportFailed(
709+
"Compression unavailable: gzip not found at \(gzipPath). " +
710+
"Please install gzip or disable compression in export options."
711+
)
712+
}
713+
698714
// Create output file
699715
guard FileManager.default.createFile(atPath: destination.path, contents: nil) else {
700716
throw ExportError.fileWriteFailed(destination.path)
701717
}
702718

703719
// Use gzip to compress the file
704720
let process = Process()
705-
process.executableURL = URL(fileURLWithPath: "/usr/bin/gzip")
721+
process.executableURL = URL(fileURLWithPath: gzipPath)
706722
process.arguments = ["-c", source.path]
707723

708724
let outputFile = try FileHandle(forWritingTo: destination)

TablePro/Views/Export/ExportDialog.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,13 @@ struct ExportDialog: View {
325325
return config.format.fileExtension
326326
}
327327

328+
/// Windows reserved device names (case-insensitive)
329+
private static let windowsReservedNames: Set<String> = [
330+
"CON", "PRN", "AUX", "NUL",
331+
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
332+
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
333+
]
334+
328335
/// Validates that the filename is not empty and contains no invalid filesystem characters
329336
private var isFileNameValid: Bool {
330337
let name = config.fileName.trimmingCharacters(in: .whitespaces)
@@ -342,6 +349,17 @@ struct ExportDialog: View {
342349
name.contains("/../") || name.contains("\\..\\")
343350
guard !isPathTraversalPattern else { return false }
344351

352+
// Check for Windows reserved device names (case-insensitive)
353+
// These can cause issues if the export file is copied to Windows
354+
let baseName = name.components(separatedBy: ".").first ?? name
355+
guard !Self.windowsReservedNames.contains(baseName.uppercased()) else { return false }
356+
357+
// Prevent hidden files on Unix (starting with .)
358+
guard !name.hasPrefix(".") else { return false }
359+
360+
// Check filename length (255 bytes is common limit on most filesystems)
361+
guard name.utf8.count <= 255 else { return false }
362+
345363
return true
346364
}
347365

0 commit comments

Comments
 (0)