Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add logging for invalid cache directory path validation #4693

Merged
merged 14 commits into from
Jan 17, 2025
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- Allow hybrid SDK to set replay options tags information (#4710)
- Add threshold to always log fatal logs (#4707)

### Improvements

- Add error logging for invalid `cacheDirectoryPath` (#4693)
### Internal

- Change macros TEST and TESTCI to SENTRY_TEST and SENTRY_TEST_CI (#4712)
Expand Down
3 changes: 3 additions & 0 deletions SentryTestUtils/SentryFileManager+Test.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

NS_ASSUME_NONNULL_BEGIN

BOOL isErrorPathTooLong(NSError *error);
BOOL createDirectoryIfNotExists(NSString *path, NSError **error);
philprime marked this conversation as resolved.
Show resolved Hide resolved

SENTRY_EXTERN NSURL *launchProfileConfigFileURL(void);
SENTRY_EXTERN NSURL *_Nullable sentryLaunchConfigFileURL;

Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ - (nullable instancetype)initWithOptions:(SentryOptions *)options
dispatchQueueWrapper:dispatchQueue
error:&error];
if (error != nil) {
SENTRY_LOG_ERROR(@"Cannot init filesystem.");
SENTRY_LOG_FATAL(@"Failed to initialize file system: %@", error.localizedDescription);
return nil;
}
return [self initWithOptions:options
Expand Down
66 changes: 49 additions & 17 deletions Sources/Sentry/SentryFileManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,49 @@

NSString *const EnvelopesPathComponent = @"envelopes";

BOOL
isErrorPathTooLong(NSError *error)
{
NSError *underlyingError = NULL;
philprime marked this conversation as resolved.
Show resolved Hide resolved
if (@available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *)) {
underlyingError = error.underlyingErrors.firstObject;
}
if (underlyingError == NULL) {
id errorInUserInfo = [error.userInfo valueForKey:NSUnderlyingErrorKey];
if (errorInUserInfo && [errorInUserInfo isKindOfClass:[NSError class]]) {
underlyingError = errorInUserInfo;

Check warning on line 33 in Sources/Sentry/SentryFileManager.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileManager.m#L33

Added line #L33 was not covered by tests
}
}
if (underlyingError == NULL) {
underlyingError = error;
}
BOOL isEnameTooLong
= underlyingError.domain == NSPOSIXErrorDomain && underlyingError.code == ENAMETOOLONG;
// On older OS versions the error code is NSFileWriteUnknown
// Reference: https://developer.apple.com/forums/thread/128927?answerId=631839022#631839022
BOOL isUnknownError = underlyingError.domain == NSCocoaErrorDomain
&& underlyingError.code == NSFileWriteUnknownError;

return isEnameTooLong || isUnknownError;
}

BOOL
createDirectoryIfNotExists(NSString *path, NSError **error)
{
if (![[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:error]) {
*error = NSErrorFromSentryErrorWithUnderlyingError(kSentryErrorFileIO,
[NSString stringWithFormat:@"Failed to create the directory at path %@.", path],
*error);
return NO;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:error];
if (success) {
return YES;
}
return YES;

if (isErrorPathTooLong(*error)) {
SENTRY_LOG_FATAL(@"Failed to create directory, path is too long: %@", path);
philprime marked this conversation as resolved.
Show resolved Hide resolved
}
*error = NSErrorFromSentryErrorWithUnderlyingError(kSentryErrorFileIO,
[NSString stringWithFormat:@"Failed to create the directory at path %@.", path], *error);
return NO;
}

/**
Expand All @@ -45,15 +75,14 @@
{
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager removeItemAtPath:path error:&error]) {
if (error.code == NSFileNoSuchFileError) {
SENTRY_LOG_DEBUG(@"No file to delete at %@", path);
} else {
SENTRY_LOG_ERROR(
@"Error occurred while deleting file at %@ because of %@", path, error);
}
} else {
if ([fileManager removeItemAtPath:path error:&error]) {
SENTRY_LOG_DEBUG(@"Successfully deleted file at %@", path);
} else if (error.code == NSFileNoSuchFileError) {
SENTRY_LOG_DEBUG(@"No file to delete at %@", path);
} else if (isErrorPathTooLong(error)) {
SENTRY_LOG_FATAL(@"Failed to remove file, path is too long: %@", path);
} else {
SENTRY_LOG_ERROR(@"Error occurred while deleting file at %@ because of %@", path, error);
}
}

Expand Down Expand Up @@ -102,9 +131,12 @@
[self removeFileAtPath:self.eventsPath];

if (!createDirectoryIfNotExists(self.sentryPath, error)) {
SENTRY_LOG_FATAL(@"Failed to create Sentry SDK working directory: %@", self.sentryPath);
return nil;
}
if (!createDirectoryIfNotExists(self.envelopesPath, error)) {
SENTRY_LOG_FATAL(
@"Failed to create Sentry SDK envelopes directory: %@", self.envelopesPath);

Check warning on line 139 in Sources/Sentry/SentryFileManager.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryFileManager.m#L139

Added line #L139 was not covered by tests
return nil;
}

Expand Down
199 changes: 198 additions & 1 deletion Tests/SentryTests/Helper/SentryFileManagerTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// swiftlint:disable file_length

@testable import Sentry
import SentryTestUtils
import XCTest
Expand Down Expand Up @@ -69,7 +71,27 @@
sut.setDelegate(delegate)
return sut
}


func getValidPath() -> String {
URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("SentryTest")
.path
}

func getTooLongPath() -> String {
var url = URL(fileURLWithPath: NSTemporaryDirectory())
for element in ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] {
url = url.appendingPathComponent(Array(
repeating: element,
count: Int(NAME_MAX)
).joined())
}
return url.path
}

func getInvalidPath() -> String {
URL(fileURLWithPath: "/dev/null").path
}
}

private var fixture: Fixture!
Expand Down Expand Up @@ -723,6 +745,181 @@
try "garbage".write(to: URL(fileURLWithPath: sut.timezoneOffsetFilePath), atomically: true, encoding: .utf8)
XCTAssertNil(sut.readTimezoneOffset())
}

func testIsErrorPathTooLong_underlyingErrorsAvailableAndMultipleErrorsGiven_shouldUseErrorInUserInfo() throws {
// -- Arrange --
guard #available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *) else {
throw XCTSkip("This test is only for macOS 11 and above")

Check warning on line 752 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L752

Added line #L752 was not covered by tests
}
// When accessing via `underlyingErrors`, the first result is the error set with `NSUnderlyingErrorKey`.
// This test asserts if that behavior changes.
let error = NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [
NSMultipleUnderlyingErrorsKey: [
NSError(domain: NSCocoaErrorDomain, code: 2, userInfo: nil)
],
NSUnderlyingErrorKey: NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG), userInfo: nil)
])
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --
XCTAssertTrue(result)
}

func testIsErrorPathTooLong_underlyingErrorsAvailableAndMultipleErrorsEmpty_shouldUseErrorInUserInfo() throws {
// -- Arrange --
guard #available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *) else {
throw XCTSkip("Test is disabled for this OS version")

Check warning on line 771 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L771

Added line #L771 was not covered by tests
}
// When accessing via `underlyingErrors`, the first result is the error set with `NSUnderlyingErrorKey`.
// This test asserts if that behavior changes.
let error = NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [
NSMultipleUnderlyingErrorsKey: [Any](),
NSUnderlyingErrorKey: NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG), userInfo: nil)
])
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --
XCTAssertTrue(result)
}

func testIsErrorPathTooLong_underlyingErrorsAvailableAndMultipleErrorsNotSet_shouldUseErrorInUserInfo() throws {
// -- Arrange --
guard #available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *) else {
throw XCTSkip("Test is disabled for this OS version")

Check warning on line 788 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L788

Added line #L788 was not covered by tests
}
// When accessing via `underlyingErrors`, the first result is the error set with `NSUnderlyingErrorKey`.
// This test asserts if that behavior changes.
let error = NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [
NSUnderlyingErrorKey: NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG), userInfo: nil)
])
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --
XCTAssertTrue(result)
}

func testIsErrorPathTooLong_underlyingErrorsAvailableAndOnlyMultipleErrorsGiven_shouldUseErrorFirstError() throws {
// -- Arrange --
guard #available(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5, *) else {
throw XCTSkip("Test is disabled for this OS version")

Check warning on line 804 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L804

Added line #L804 was not covered by tests
}
// When accessing via `underlyingErrors`, the first result is the error set with `NSUnderlyingErrorKey`.
// This test asserts if that behavior changes.
let error = NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [
NSMultipleUnderlyingErrorsKey: [
NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG), userInfo: nil),
NSError(domain: NSCocoaErrorDomain, code: 2, userInfo: nil)
]
])
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --
XCTAssertTrue(result)
}

func testIsErrorPathTooLong_underlyingErrorsNotAvailableAndErrorNotInUserInfo_shouldNotCheckError() throws {
// -- Arrange --
guard #unavailable(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5) else {
throw XCTSkip("Test is disabled for this OS version")
}
// When accessing via `underlyingErrors`, the first result is the error set with `NSUnderlyingErrorKey`.
// This test asserts if that behavior changes.
let error = NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [:])
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --

Check warning on line 830 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L825-L830

Added lines #L825 - L830 were not covered by tests
XCTAssertFalse(result)
}

Check warning on line 832 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L832

Added line #L832 was not covered by tests

func testIsErrorPathTooLong_underlyingErrorsNotAvailableAndNonErrorInUserInfo_shouldNotCheckError() throws {
// -- Arrange --
guard #unavailable(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5) else {
throw XCTSkip("Test is disabled for this OS version")
}
// When accessing via `underlyingErrors`, the first result is the error set with `NSUnderlyingErrorKey`.
// This test asserts if that behavior changes.
let error = NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [
NSUnderlyingErrorKey: "This is not an error"
])
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --

Check warning on line 846 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L839-L846

Added lines #L839 - L846 were not covered by tests
XCTAssertFalse(result)
}

Check warning on line 848 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L848

Added line #L848 was not covered by tests

func testIsErrorPathTooLong_underlyingErrorsNotAvailableAndErrorInUserInfo_shouldNotCheckError() throws {
// -- Arrange --
guard #unavailable(macOS 11.3, iOS 14.5, watchOS 7.4, tvOS 14.5) else {
throw XCTSkip("Test is disabled for this OS version")
}
// When accessing via `underlyingErrors`, the first result is the error set with `NSUnderlyingErrorKey`.
// This test asserts if that behavior changes.
let error = NSError(domain: NSCocoaErrorDomain, code: 1, userInfo: [
NSUnderlyingErrorKey: NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG), userInfo: nil)
])
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --

Check warning on line 862 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L855-L862

Added lines #L855 - L862 were not covered by tests
XCTAssertTrue(result)
}

Check warning on line 864 in Tests/SentryTests/Helper/SentryFileManagerTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Helper/SentryFileManagerTests.swift#L864

Added line #L864 was not covered by tests

func testIsErrorPathTooLong_errorIsEnameTooLong_shouldReturnTrue() throws {
// -- Arrange --
let error = NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG), userInfo: nil)
// -- Act --
let result = isErrorPathTooLong(error)
// -- Assert --
XCTAssertTrue(result)
}

func testCreateDirectoryIfNotExists_successful_shouldNotLogError() throws {
// -- Arrange -
let logOutput = TestLogOutput()
SentryLog.setLogOutput(logOutput)
SentryLog.configureLog(true, diagnosticLevel: .debug)

let path = fixture.getValidPath()
var error: NSError?
// -- Act --
let result = createDirectoryIfNotExists(path, &error)
// -- Assert -
XCTAssertTrue(result)
XCTAssertEqual(logOutput.loggedMessages.count, 0)
}

func testCreateDirectoryIfNotExists_pathTooLogError_shouldLogError() throws {
// -- Arrange -
let logOutput = TestLogOutput()
SentryLog.setLogOutput(logOutput)
SentryLog.configureLog(true, diagnosticLevel: .debug)

let path = fixture.getTooLongPath()
var error: NSError?
// -- Act --
let result = createDirectoryIfNotExists(path, &error)
// -- Assert -
XCTAssertFalse(result)
XCTAssertEqual(error?.domain, SentryErrorDomain)
XCTAssertEqual(error?.code, 108)
XCTAssertEqual(logOutput.loggedMessages.count, 1)
}

func testCreateDirectoryIfNotExists_otherError_shouldNotLogError() throws {
// -- Arrange -
let logOutput = TestLogOutput()
SentryLog.setLogOutput(logOutput)
SentryLog.configureLog(true, diagnosticLevel: .debug)

let path = fixture.getInvalidPath()
var error: NSError?
// -- Act --
let result = createDirectoryIfNotExists(path, &error)
// -- Assert -
XCTAssertFalse(result)
XCTAssertEqual(error?.domain, SentryErrorDomain)
XCTAssertEqual(error?.code, 108)
XCTAssertEqual(logOutput.loggedMessages.count, 0)
}
}

#if os(iOS) || os(macOS) || targetEnvironment(macCatalyst)
Expand Down
Loading