Skip to content
69 changes: 65 additions & 4 deletions Sources/Panels/FilePreviewPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,10 @@ enum FilePreviewKindResolver {

private static func initialResolution(for url: URL) -> Resolution {
let ext = url.pathExtension.lowercased()
if needsSniffBeforeTextOrMedia(url: url) {
return .needsSniff
}

if let type = UTType(filenameExtension: ext),
let mediaMode = mediaMode(for: type) {
return .resolved(mediaMode)
Expand All @@ -731,6 +735,19 @@ enum FilePreviewKindResolver {
return .resolved(.quickLook)
}

if needsSniffBeforeTextOrMedia(url: url) {
if sniffLooksLikeText(url: url) {
return .resolved(.text)
}
if looksLikeMPEGTransportStream(url: url) {
return .resolved(.media)
}
if let mediaMode = contentTypes(for: url).lazy.compactMap({ mediaMode(for: $0) }).first {
return .resolved(mediaMode)
}
return .needsSniff
}
Comment thread
cursor[bot] marked this conversation as resolved.

for type in contentTypes(for: url) {
if let mediaMode = mediaMode(for: type) {
return .resolved(mediaMode)
Expand Down Expand Up @@ -793,28 +810,72 @@ enum FilePreviewKindResolver {
return false
}

private static func needsSniffBeforeTextOrMedia(url: URL) -> Bool {
let filename = url.lastPathComponent.lowercased()
let ext = url.pathExtension.lowercased()
if ext == "ts" {
return true
}
guard textFilenames.contains(filename) || textExtensions.contains(ext),
let type = UTType(filenameExtension: ext) else {
return false
}

return mediaMode(for: type) != nil
&& !type.conforms(to: .text)
&& !type.conforms(to: .sourceCode)
}

private static func looksLikeBinaryPropertyList(url: URL) -> Bool {
guard let handle = try? FileHandle(forReadingFrom: url) else { return false }
defer { try? handle.close() }
let data = (try? handle.read(upToCount: 8)) ?? Data()
return String(data: data, encoding: .ascii) == "bplist00"
}

private static func looksLikeMPEGTransportStream(url: URL) -> Bool {
guard url.pathExtension.lowercased() == "ts",
let handle = try? FileHandle(forReadingFrom: url) else { return false }
defer { try? handle.close() }

let data = (try? handle.read(upToCount: 4096)) ?? Data()
guard data.count >= 376 else { return false }

let syncCandidates = [
(packetSize: 188, syncOffset: 0),
(packetSize: 192, syncOffset: 0),
(packetSize: 192, syncOffset: 4),
(packetSize: 204, syncOffset: 0)
]

for candidate in syncCandidates where data.count > candidate.syncOffset {
var offset = candidate.syncOffset
var syncCount = 0
while offset < data.count {
guard data[offset] == 0x47 else { break }
syncCount += 1
offset += candidate.packetSize
}
if syncCount >= 2 {
return true
}
}

return false
}

private static func sniffLooksLikeText(url: URL) -> Bool {
guard let handle = try? FileHandle(forReadingFrom: url) else { return false }
defer { try? handle.close() }
let data = (try? handle.read(upToCount: 4096)) ?? Data()
guard !data.isEmpty else { return true }
if String(data: data, encoding: .utf8) != nil {
return true
}
if hasUTF16ByteOrderMark(data), String(data: data, encoding: .utf16) != nil {
return true
}
if data.contains(0) {
return false
}
return false
return String(data: data, encoding: .utf8) != nil
}

private static func hasUTF16ByteOrderMark(_ data: Data) -> Bool {
Expand Down
112 changes: 112 additions & 0 deletions cmuxTests/FilePreviewReviewFeedbackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,118 @@ final class FilePreviewReviewFeedbackTests: XCTestCase {
XCTAssertEqual(FilePreviewKindResolver.mode(for: url), .text)
}

func testExtensionlessANSITextResolvesAsTextAfterSniffing() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: url) }
try "\u{001B}[31mred\u{001B}[0m\n".write(to: url, atomically: true, encoding: .utf8)

XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook)
XCTAssertEqual(FilePreviewKindResolver.mode(for: url), .text)
}

func testTypeScriptFileResolvesAsTextInsteadOfTransportStreamMedia() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("ts")
defer { try? FileManager.default.removeItem(at: url) }

try """
export const answer: number = 42;
console.log(answer);
""".write(to: url, atomically: true, encoding: .utf8)

XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook)
XCTAssertEqual(FilePreviewKindResolver.mode(for: url), .text)
}

func testUTF8BOMTypeScriptFileResolvesAsText() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("ts")
defer { try? FileManager.default.removeItem(at: url) }

var data = Data([0xEF, 0xBB, 0xBF])
data.append(Data("export const answer: number = 42;\n".utf8))
try data.write(to: url, options: .atomic)

XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook)
XCTAssertEqual(FilePreviewKindResolver.mode(for: url), .text)
}

func testTypeScriptFileWithNULBytesDoesNotResolveAsText() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("ts")
defer { try? FileManager.default.removeItem(at: url) }

var data = Data("export const answer = 42;".utf8)
data.append(contentsOf: [0x00, 0x00])
try data.write(to: url, options: .atomic)

XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook)
XCTAssertNotEqual(FilePreviewKindResolver.mode(for: url), .text)
}

func testTypeScriptTextWinsOverTransportStreamSyncBytePattern() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("ts")
defer { try? FileManager.default.removeItem(at: url) }

let source = "G"
+ String(repeating: "a", count: 187)
+ "G"
+ String(repeating: "b", count: 187)
+ "\nexport const answer: number = 42;\n"
try source.write(to: url, atomically: true, encoding: .utf8)

XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook)
XCTAssertEqual(FilePreviewKindResolver.mode(for: url), .text)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func testBinaryTransportStreamFileKeepsMediaPreview() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("ts")
defer { try? FileManager.default.removeItem(at: url) }

var data = Data(repeating: 0, count: 188 * 2)
data[0] = 0x47
data[1] = 0x40
data[2] = 0x00
data[3] = 0x10
data[188] = 0x47
data[189] = 0x41
data[190] = 0x00
data[191] = 0x10
try data.write(to: url, options: .atomic)

XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook)
XCTAssertEqual(FilePreviewKindResolver.mode(for: url), .media)
}

func testM2TSTransportStreamFileKeepsMediaPreview() throws {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("ts")
defer { try? FileManager.default.removeItem(at: url) }

var data = Data(repeating: 0, count: 192 * 2)
data[4] = 0x47
data[5] = 0x40
data[6] = 0x00
data[7] = 0x10
data[196] = 0x47
data[197] = 0x41
data[198] = 0x00
data[199] = 0x10
try data.write(to: url, options: .atomic)

XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook)
XCTAssertEqual(FilePreviewKindResolver.mode(for: url), .media)
}
Comment on lines +141 to +181

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 initialMode for a binary .ts file is not tested

After this PR, initialResolution routes ALL .ts files through .needsSniff (because knownTextFileNeedsSniffBeforeMedia returns true for any .ts), so a binary MPEG-TS file now shows .quickLook initially rather than .media. A XCTAssertEqual(FilePreviewKindResolver.initialMode(for: url), .quickLook) assertion in the binary test would confirm this is intentional and guard against future regressions.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. The binary .ts regression now asserts the intentional initial Quick Look mode before resolved media mode.

— Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in the transport-stream tests: both binary .ts fixtures now assert initialMode is quickLook before resolving to media.

— Claude Code


func testQuickLookSessionCloseDoesNotDeactivateMountedRepresentableView() throws {
let url = try temporaryBinaryFile()
defer { try? FileManager.default.removeItem(at: url) }
Expand Down
Loading