33//
44// Created by Claude on 03.03.2026.
55// Copyright © 2026 Senatov. All rights reserved.
6- // Description: Lightweight encrypted archive detection for icon display.
6+ // Description: Encrypted archive detection for icon display.
77// ZIP: reads 8 bytes from Local File Header (bit 0 of General Purpose Flag).
8- // Other formats: not checked (would require ArchiveKit dependency).
9- // Results cached via NSCache — zero repeated I/O cost.
8+ // 7z: runs `7z l -slt` — checks exit code and "Encrypted = +" marker.
9+ // RAR: runs `unrar lt` — checks for encryption markers, fallback to 7z.
10+ // Results cached via NSCache — zero repeated I/O cost after first check.
1011
1112import FileModelKit
1213import Foundation
@@ -16,28 +17,105 @@ enum EncryptedArchiveCheck {
1617 // MARK: - Cache (NSCache is internally thread-safe)
1718 nonisolated ( unsafe) private static let cache = NSCache < NSString , NSNumber > ( )
1819 // MARK: - Public API
19- /// Returns true if archive is encrypted. Currently supports ZIP only.
20- /// Returns false for non- ZIP archives (not nil — safe for icon logic ).
20+ /// Returns true if archive is encrypted.
21+ /// Supports ZIP (instant), 7z, RAR (via shell, cached ).
2122 static func isEncrypted( url: URL ) -> Bool {
2223 let key = url. path as NSString
2324 if let cached = cache. object ( forKey: key) {
2425 return cached. boolValue
2526 }
26- let result = checkZip ( url: url)
27+ let result = detect ( url: url)
2728 cache. setObject ( NSNumber ( value: result) , forKey: key)
2829 return result
2930 }
30- // MARK: - ZIP Check
31+ /// Invalidate cache for a specific file (e.g. after modification)
32+ static func invalidate( url: URL ) {
33+ cache. removeObject ( forKey: url. path as NSString )
34+ }
35+ // MARK: - Detection Router
36+ private static func detect( url: URL ) -> Bool {
37+ let ext = url. pathExtension. lowercased ( )
38+ switch ext {
39+ case " zip " :
40+ return checkZipHeader ( url: url) || checkVia7z ( url: url)
41+ case " 7z " :
42+ return checkVia7z ( url: url)
43+ case " rar " :
44+ return checkViaUnrar ( url: url) ?? checkVia7z ( url: url)
45+ default :
46+ // Other archive formats handled by 7z (cab, arj, etc.)
47+ if ArchiveExtensions . isArchive ( ext) {
48+ return checkVia7z ( url: url)
49+ }
50+ return false
51+ }
52+ }
53+ // MARK: - ZIP Header Check
3154 /// Reads PK signature + General Purpose Bit Flag. Bit 0 = encrypted.
32- private static func checkZip ( url : URL ) -> Bool {
33- guard url . pathExtension . lowercased ( ) == " zip " else { return false }
55+ /// Cost: 8 bytes, instant.
56+ private static func checkZipHeader ( url : URL ) -> Bool {
3457 guard let handle = try ? FileHandle ( forReadingFrom: url) else { return false }
3558 defer { try ? handle. close ( ) }
3659 guard let data = try ? handle. read ( upToCount: 8 ) , data. count >= 8 else { return false }
37- // Verify PK\x03\x04 signature
3860 guard data [ 0 ] == 0x50 , data [ 1 ] == 0x4B , data [ 2 ] == 0x03 , data [ 3 ] == 0x04 else { return false }
39- // General Purpose Bit Flag at offset 6 (little-endian)
4061 let flags = UInt16 ( data [ 6 ] ) | ( UInt16 ( data [ 7 ] ) << 8 )
4162 return ( flags & 0x0001 ) != 0
4263 }
64+ // MARK: - 7z Shell Check
65+ /// Runs `7z l -slt <archive>`. Encrypted header → exit != 0.
66+ /// Encrypted content → output contains "Encrypted = +".
67+ private static func checkVia7z( url: URL ) -> Bool {
68+ guard let bin = findExecutable ( " 7z " ) else { return false }
69+ let ( exit, stdout, stderr) = shell ( bin, " l " , " -slt " , url. path)
70+ // Non-zero exit with password/encrypted message → header-encrypted
71+ if exit != 0 {
72+ let combined = ( stdout + stderr) . lowercased ( )
73+ if combined. contains ( " password " ) || combined. contains ( " encrypted " ) || combined. contains ( " headers error " ) {
74+ return true
75+ }
76+ return false
77+ }
78+ // Content-encrypted: individual entries marked
79+ return stdout. contains ( " Encrypted = + " )
80+ }
81+ // MARK: - RAR Shell Check
82+ /// Runs `unrar lt <archive>`. Returns nil if unrar not found.
83+ private static func checkViaUnrar( url: URL ) -> Bool ? {
84+ guard let bin = findExecutable ( " unrar " ) else { return nil }
85+ let ( exit, stdout, stderr) = shell ( bin, " lt " , url. path)
86+ let combined = ( stdout + stderr) . lowercased ( )
87+ if exit != 0 && ( combined. contains ( " encrypted " ) || combined. contains ( " password " ) ) {
88+ return true
89+ }
90+ if stdout. lowercased ( ) . contains ( " encrypted " ) {
91+ return true
92+ }
93+ if exit == 0 { return false }
94+ return nil // inconclusive, let caller fallback to 7z
95+ }
96+ // MARK: - Shell Helper
97+ private static func shell( _ args: String ... ) -> ( Int32 , String , String ) {
98+ let proc = Process ( )
99+ let outPipe = Pipe ( )
100+ let errPipe = Pipe ( )
101+ proc. executableURL = URL ( fileURLWithPath: args [ 0 ] )
102+ proc. arguments = Array ( args. dropFirst ( ) )
103+ proc. standardOutput = outPipe
104+ proc. standardError = errPipe
105+ proc. environment = ProcessInfo . processInfo. environment
106+ do { try proc. run ( ) } catch { return ( - 1 , " " , error. localizedDescription) }
107+ proc. waitUntilExit ( )
108+ let outData = outPipe. fileHandleForReading. readDataToEndOfFile ( )
109+ let errData = errPipe. fileHandleForReading. readDataToEndOfFile ( )
110+ return (
111+ proc. terminationStatus,
112+ String ( data: outData, encoding: . utf8) ?? " " ,
113+ String ( data: errData, encoding: . utf8) ?? " "
114+ )
115+ }
116+ // MARK: - Find Executable
117+ private static func findExecutable( _ name: String ) -> String ? {
118+ [ " /opt/homebrew/bin/ \( name) " , " /usr/local/bin/ \( name) " , " /usr/bin/ \( name) " ]
119+ . first { FileManager . default. isExecutableFile ( atPath: $0) }
120+ }
43121}
0 commit comments