Skip to content

Commit 9c4cb9b

Browse files
authored
Fix potential crash in #177 (#178)
* Switched OutputFileStream to use FileHandle * updated Storage for OutputFileStream changes * Added unableToClose handler * Fixed test issue w/ parallelized tests. * Removed dead code * Flush memory contents to file before closing. * Fix lack of version callouts. * Added version checks for sync/close
1 parent 36a1546 commit 9c4cb9b

File tree

4 files changed

+50
-25
lines changed

4 files changed

+50
-25
lines changed

Sources/Segment/Errors.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public enum AnalyticsError: Error {
1212
case storageUnableToWrite(String)
1313
case storageUnableToRename(String)
1414
case storageUnableToOpen(String)
15+
case storageUnableToClose(String)
1516
case storageInvalid(String)
1617
case storageUnknown(Error)
1718

@@ -41,6 +42,8 @@ extension Analytics {
4142
return AnalyticsError.storageUnableToOpen(path)
4243
case .unableToWrite(let path):
4344
return AnalyticsError.storageUnableToWrite(path)
45+
case .unableToClose(let path):
46+
return AnalyticsError.storageUnableToClose(path)
4447
}
4548
}
4649

Sources/Segment/Utilities/OutputFileStream.swift

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ internal class OutputFileStream {
2121
case unableToOpen(String)
2222
case unableToWrite(String)
2323
case unableToCreate(String)
24+
case unableToClose(String)
2425
}
2526

26-
var filePointer: UnsafeMutablePointer<FILE>? = nil
27+
var fileHandle: FileHandle? = nil
2728
let fileURL: URL
2829

2930
init(fileURL: URL) throws {
@@ -49,36 +50,49 @@ internal class OutputFileStream {
4950

5051
/// Open simply opens the file, no attempt at creation is made.
5152
func open() throws {
52-
if filePointer != nil { return }
53-
let path = fileURL.path
54-
path.withCString { file in
55-
filePointer = fopen(file, "w")
53+
if fileHandle != nil { return }
54+
do {
55+
fileHandle = try FileHandle(forWritingTo: fileURL)
56+
} catch {
57+
throw OutputStreamError.unableToOpen(fileURL.path)
5658
}
57-
guard filePointer != nil else { throw OutputStreamError.unableToOpen(path) }
5859
}
5960

6061
func write(_ data: Data) throws {
61-
guard let string = String(data: data, encoding: .utf8) else { return }
62-
try write(string)
62+
guard data.isEmpty == false else { return }
63+
if #available(macOS 10.15.4, iOS 13.4, macCatalyst 13.4, tvOS 13.4, watchOS 13.4, *) {
64+
do {
65+
try fileHandle?.write(contentsOf: data)
66+
} catch {
67+
throw OutputStreamError.unableToWrite(fileURL.path)
68+
}
69+
} else {
70+
// Fallback on earlier versions
71+
fileHandle?.write(data)
72+
}
6373
}
6474

6575
func write(_ string: String) throws {
6676
guard string.isEmpty == false else { return }
67-
_ = try string.utf8.withContiguousStorageIfAvailable { str in
68-
if let baseAddr = str.baseAddress {
69-
fwrite(baseAddr, 1, str.count, filePointer)
70-
} else {
71-
throw OutputStreamError.unableToWrite(fileURL.path)
72-
}
73-
if ferror(filePointer) != 0 {
74-
throw OutputStreamError.unableToWrite(fileURL.path)
75-
}
77+
if let data = string.data(using: .utf8) {
78+
try write(data)
7679
}
7780
}
7881

79-
func close() {
80-
fclose(filePointer)
81-
filePointer = nil
82+
func close() throws {
83+
do {
84+
let existing = fileHandle
85+
fileHandle = nil
86+
if #available(tvOS 13.0, *) {
87+
try existing?.synchronize() // this might be overkill, but JIC.
88+
try existing?.close()
89+
} else {
90+
// Fallback on earlier versions
91+
existing?.synchronizeFile()
92+
existing?.closeFile()
93+
}
94+
} catch {
95+
throw OutputStreamError.unableToClose(fileURL.path)
96+
}
8297
}
8398
}
84-

Sources/Segment/Utilities/Storage.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,11 @@ extension Storage {
321321
let fileEnding = "],\"sentAt\":\"\(sentAt)\",\"writeKey\":\"\(writeKey)\"}"
322322
do {
323323
try outputStream.write(fileEnding)
324+
try outputStream.close()
324325
} catch {
325326
analytics?.reportInternalError(error)
326327
}
327-
outputStream.close()
328+
328329
self.outputStream = nil
329330

330331
let tempFile = file.appendingPathExtension(Storage.tempExtension)

Tests/Segment-Tests/Analytics_Tests.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,16 +533,23 @@ final class Analytics_Tests: XCTestCase {
533533
}
534534

535535
func testRequestFactory() {
536-
let config = Configuration(writeKey: "test").requestFactory { request in
536+
let config = Configuration(writeKey: "testSequential").requestFactory { request in
537537
XCTAssertEqual(request.value(forHTTPHeaderField: "Accept-Encoding"), "gzip")
538538
XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/json; charset=utf-8")
539-
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Basic test")
539+
XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Basic testSequential")
540540
XCTAssertTrue(request.value(forHTTPHeaderField: "User-Agent")!.contains("analytics-ios/"))
541541
return request
542542
}.errorHandler { error in
543-
XCTFail("\(error)")
543+
switch error {
544+
case AnalyticsError.networkServerRejected(_):
545+
// we expect this one; it's a bogus writekey
546+
break;
547+
default:
548+
XCTFail("\(error)")
549+
}
544550
}
545551
let analytics = Analytics(configuration: config)
552+
analytics.storage.hardReset(doYouKnowHowToUseThis: true)
546553
let outputReader = OutputReaderPlugin()
547554
analytics.add(plugin: outputReader)
548555

0 commit comments

Comments
 (0)