feat(mac): ensure that home directory is not written to logs

also replace references to URL.path with
URL.path(percentEncoded: false) because FileManager
operation do not expect percent encoding
This commit is contained in:
Shawn Schantz 2026-09-02 15:36:19 -04:00
parent af6806666a
commit 7d622997cb
8 changed files with 78 additions and 54 deletions

View file

@ -58,15 +58,15 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega
// get the package id (though it appears to be identifying a keyboard in the URL)
let matchPackageId = String(match.4)
if let downloadUrl = self.settings?.buildDownloadPackageUrl(for: matchPackageId) {
Logger.download.info("package install, download url = \(downloadUrl.absoluteString, privacy: .public)")
LogUtil.infoBreadcrumb("package install, download url = \(downloadUrl.absoluteString)", category: .download)
Logger.download.info("package install, download url = \(downloadUrl.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("package install, download url = \(downloadUrl.cleanUrlPath())", category: .download)
let newRequest = URLRequest(url: downloadUrl)
DispatchQueue.main.async {
webView.startDownload(using: newRequest) { download in
Logger.download.info("download initiated to \(newRequest.url?.absoluteString ?? "nil", privacy: .public)")
LogUtil.infoBreadcrumb("download initiated to \(newRequest.url?.absoluteString ?? "nil")", category: .download)
Logger.download.info("download initiated to \(newRequest.url?.cleanUrlPath() ?? "nil", privacy: .public)")
LogUtil.infoBreadcrumb("download initiated to \(newRequest.url?.cleanUrlPath() ?? "nil")", category: .download)
download.delegate = self
self.setupDownloadTracking(download)
}
@ -165,8 +165,8 @@ public class DownloadCoordinator: NSObject, ObservableObject, WKNavigationDelega
self.progressObserver = nil
if let downloadDestination = installHelper?.temporaryKmpFileLocation {
Logger.download.info("download of \(downloadDestination.path, privacy: .public) was successful.")
LogUtil.infoBreadcrumb("download of \(downloadDestination.path) was successful.", category: .download)
Logger.download.info("download of \(downloadDestination.cleanUrlPath(), privacy: .public) was successful.")
LogUtil.infoBreadcrumb("download of \(downloadDestination.cleanUrlPath()) was successful.", category: .download)
if let settings {
do {

View file

@ -56,7 +56,7 @@ public class InputMethodUtil {
return false
}
return FileManager.default.fileExists(atPath: inputMethodUrl.path)
return FileManager.default.fileExists(atPath: inputMethodUrl.path(percentEncoded: false))
}
/**
@ -214,8 +214,8 @@ public class InputMethodUtil {
let process = Process()
if let executableUrl = self.pathUtil.buildInputMethodExecutableUrl(fileName: self.keymanInputMethodApplicationName) {
process.executableURL = executableUrl
Logger.setup.info("invoking Keyman at: \(String(describing: process.executableURL), privacy: .public)")
LogUtil.infoBreadcrumb("invoking Keyman at: \(String(describing: process.executableURL))", category: .setup)
Logger.setup.info("invoking Keyman at: \(String(describing: process.executableURL?.cleanUrlPath()), privacy: .public)")
LogUtil.infoBreadcrumb("invoking Keyman at: \(String(describing: process.executableURL?.cleanUrlPath()))", category: .setup)
process.arguments = [argument]
}
@ -253,8 +253,8 @@ public class InputMethodUtil {
NSWorkspace.shared.openApplication(at: inputMethodUrl, configuration: openConfig) { (app, error) in
if let error = error {
Logger.setup.error("Could not launch Keyman input method at \(inputMethodUrl), due to error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("Could not launch Keyman input method at \(inputMethodUrl), due to error: \(error as NSError)", category: .setup)
Logger.setup.error("Could not launch Keyman input method at \(inputMethodUrl.cleanUrlPath()), due to error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("Could not launch Keyman input method at \(inputMethodUrl.cleanUrlPath()), due to error: \(error as NSError)", category: .setup)
}
}
}

View file

@ -8,6 +8,31 @@
import Sentry
import Foundation
/**
* Extend URL to add function that cleans path strings, removing the home directory from the path.
*/
extension URL {
/**
* If the path contains the user's home, replace it with ~ as the home directory name
* may contain the user's name and should not be written to the logs.
*/
public func cleanUrlPath() -> String {
guard self.isFileURL else { return self.absoluteString }
let unescapedPath = self.path(percentEncoded: false)
let homeDirectory = NSHomeDirectory()
if unescapedPath.hasPrefix(homeDirectory) {
let relativeComponent = unescapedPath.dropFirst(homeDirectory.count)
return "~\(relativeComponent)"
}
return unescapedPath
}
}
public struct LogUtil {
public enum LogCategory: String {
case setup // related to start of app and installation

View file

@ -153,7 +153,6 @@ public class SettingsContainer : ObservableObject {
Logger.data.log("Found documents group container")
} catch KeymanPathError.groupContainerNotFound {
Logger.data.error("Document group container not found")
let message = "Document group container not found."
fatalError("Document group container not found.")
} catch {
Logger.data.error("Unable to access documents in group container, error \(error as NSError, privacy: .public)")
@ -483,8 +482,8 @@ public class SettingsContainer : ObservableObject {
* Delegates to the PackageInstallHelper instance to decide whether the package should be installed.
*/
public func packageDownloadComplete(kmpFileUrl: URL) throws {
Logger.setup.info("packageDownloadComplete \(kmpFileUrl, privacy: .public)")
LogUtil.infoBreadcrumb("packageDownloadComplete \(kmpFileUrl)", category: .setup)
Logger.setup.info("packageDownloadComplete \(kmpFileUrl.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("packageDownloadComplete \(kmpFileUrl.cleanUrlPath())", category: .setup)
do {
try self.packageInstall?.prepareToInstall(for: kmpFileUrl)

View file

@ -85,9 +85,9 @@ public class Keyboard: Identifiable, Hashable, Equatable {
* validate whether a corresponding kmx file exists for this keyboard
*/
public func validateKmxFile(in packageDirectory: URL) throws {
let kmxFilePath = self.deriveKmxFileUrl(from: packageDirectory).path
if !FileManager.default.fileExists(atPath: kmxFilePath) {
Logger.data.error("error: could not find kmx file \(kmxFilePath, privacy: .public)")
let kmxFilePath = self.deriveKmxFileUrl(from: packageDirectory)
if !FileManager.default.fileExists(atPath: kmxFilePath.path(percentEncoded: false)) {
Logger.data.error("error: could not find kmx file \(kmxFilePath.cleanUrlPath(), privacy: .public)")
throw LoadPackageError.missingKmxFile
}
}

View file

@ -49,7 +49,7 @@ public struct KeymanPaths {
// if for some reason it doesn't exist, create it
let fileManager = FileManager.default
if !fileManager.fileExists(atPath: fontsDirectory.path) {
if !fileManager.fileExists(atPath: fontsDirectory.path(percentEncoded: false)) {
do {
try fileManager.createDirectory(at: fontsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
@ -113,16 +113,16 @@ public struct KeymanPaths {
}
fileprivate func logPaths() {
Logger.setup.debug("documents: \(self.keyman17DocumentsDirectory!.absoluteString)")
Logger.setup.debug("keyman 17 packages: \(self.keyman17PackagesDirectory!.absoluteString)")
Logger.setup.debug("documents: \(self.keyman17DocumentsDirectory!.cleanUrlPath())")
Logger.setup.debug("keyman 17 packages: \(self.keyman17PackagesDirectory!.cleanUrlPath())")
Logger.setup.debug("support directory: \(self.keyman18SupportDirectory!.absoluteString)")
Logger.setup.debug("support keyman directory: \(self.keyman18DataDirectory!.absoluteString)")
Logger.setup.debug("keyman 18 packages: \(self.keyman18PackagesDirectory!.absoluteString)")
Logger.setup.debug("support directory: \(self.keyman18SupportDirectory!.cleanUrlPath())")
Logger.setup.debug("support keyman directory: \(self.keyman18DataDirectory!.cleanUrlPath())")
Logger.setup.debug("keyman 18 packages: \(self.keyman18PackagesDirectory!.cleanUrlPath())")
Logger.setup.debug("container: \(self.keyman19ContainerDirectory.absoluteString)")
Logger.setup.debug("preferences: \(self.keyman19PreferencesDirectory.absoluteString)")
Logger.setup.debug("keyman 19 packages: \(self.keyman19PackagesDirectory.absoluteString)")
Logger.setup.debug("container: \(self.keyman19ContainerDirectory.cleanUrlPath())")
Logger.setup.debug("preferences: \(self.keyman19PreferencesDirectory.cleanUrlPath())")
Logger.setup.debug("keyman 19 packages: \(self.keyman19PackagesDirectory.cleanUrlPath())")
}
/**

View file

@ -70,7 +70,7 @@ public class PackageInstallHelper: Identifiable {
* Indicates that a package has been downloaded and can be prepared for installation
*/
public func packageDownloadComplete(for kmpFileUrl: URL) throws {
Logger.data.log("packageDownloadComplete \(kmpFileUrl.path, privacy: .public)")
Logger.data.log("packageDownloadComplete \(kmpFileUrl.cleanUrlPath(), privacy: .public)")
try self.prepareToInstall(for: kmpFileUrl)
}
@ -80,7 +80,7 @@ public class PackageInstallHelper: Identifiable {
*
*/
public func prepareToInstall(for kmpFileUrl: URL) throws {
Logger.data.log("prepareToInstall \(kmpFileUrl.path, privacy: .public)")
Logger.data.log("prepareToInstall \(kmpFileUrl.cleanUrlPath(), privacy: .public)")
do {
// unzip to the temp directory
@ -104,8 +104,8 @@ public class PackageInstallHelper: Identifiable {
self.packageInstallationType = self.determinePackageInstallationType(newPackage: package)
} catch {
self.cleanupFailedInstallation()
Logger.data.error("package installation failed for \(kmpFileUrl) with error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("package installation failed for \(kmpFileUrl) with error: \(error as NSError)", category: .data)
Logger.data.error("package installation failed for \(kmpFileUrl.cleanUrlPath(), privacy: .public) with error: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("package installation failed for \(kmpFileUrl.cleanUrlPath()) with error: \(error as NSError)", category: .data)
throw error
}
}
@ -185,7 +185,7 @@ public class PackageInstallHelper: Identifiable {
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]) }
catch {
Logger.data.error("error: unable to get contents of package fonts directory at \(installLocation.path, privacy: .public) with error: \(error as NSError, privacy: .public)")
Logger.data.error("error: unable to get contents of package fonts directory at \(installLocation.cleanUrlPath(), privacy: .public) with error: \(error as NSError, privacy: .public)")
}
for fontUrl in fileUrls {
@ -228,7 +228,7 @@ public class PackageInstallHelper: Identifiable {
let fileManager = FileManager.default
// remove the font from the fonts directory just in case it is an old one
if fileManager.fileExists(atPath: fontDestinationUrl.path) {
if fileManager.fileExists(atPath: fontDestinationUrl.path(percentEncoded: false)) {
Logger.data.info("removed existing font: \(fontDestinationUrl.lastPathComponent, privacy: .public)")
LogUtil.infoBreadcrumb("removed existing font: \(fontDestinationUrl.lastPathComponent)", category: .data)
try? fileManager.removeItem(at: fontDestinationUrl)

View file

@ -80,8 +80,8 @@ public class PackageRepository: PackageRepo {
*
*/
public func loadSinglePackage(packageUrl: URL) throws -> KeymanPackage {
Logger.data.info("loadSinglePackage from url: \(packageUrl, privacy: .public)")
LogUtil.infoBreadcrumb("loadSinglePackage from url: \(packageUrl)", category: .data)
Logger.data.info("loadSinglePackage from url: \(packageUrl.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("loadSinglePackage from url: \(packageUrl.cleanUrlPath())", category: .data)
guard let source = try readPackageFromDirectory(packageDirectoryUrl: packageUrl) else { throw LoadPackageError.invalidUrl }
@ -94,12 +94,12 @@ public class PackageRepository: PackageRepo {
* delete the package from disk
*/
public func deletePackage(package: KeymanPackage) {
Logger.data.info("deleting package: \(package.sourceDirectoryUrl, privacy: .public)")
LogUtil.infoBreadcrumb("deleting package: \(package.sourceDirectoryUrl)", category: .data)
Logger.data.info("deleting package: \(package.sourceDirectoryUrl.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("deleting package: \(package.sourceDirectoryUrl.cleanUrlPath())", category: .data)
do {
try FileManager.default.removeItem(at: package.sourceDirectoryUrl)
Logger.data.info("deleted package: \(package.sourceDirectoryUrl, privacy: .public)")
LogUtil.infoBreadcrumb("deleted package: \(package.sourceDirectoryUrl)", category: .data)
Logger.data.info("deleted package: \(package.sourceDirectoryUrl.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("deleted package: \(package.sourceDirectoryUrl.cleanUrlPath())", category: .data)
} catch {
Logger.data.error("could not delete directory: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("could not delete directory: \(error as NSError)", category: .data)
@ -115,23 +115,23 @@ public class PackageRepository: PackageRepo {
let packageTempDirectory = pathUtil.keyman19TempDirectory
// create the keyman-packages directory if it doesn't already exist
if !FileManager.default.fileExists(atPath: packageDirectory.path) {
if !FileManager.default.fileExists(atPath: packageDirectory.path(percentEncoded: false)) {
try FileManager.default.createDirectory(at: packageDirectory, withIntermediateDirectories: true, attributes: nil)
Logger.data.info("Created directory: \(packageDirectory.path, privacy: .public)")
LogUtil.infoBreadcrumb("Created directory: \(packageDirectory.path)", category: .data)
Logger.data.info("Created directory: \(packageDirectory.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("Created directory: \(packageDirectory.cleanUrlPath())", category: .data)
} else {
Logger.data.info("Directory already exists: \(packageDirectory.path, privacy: .public)")
LogUtil.infoBreadcrumb("Directory already exists: \(packageDirectory.path)", category: .data)
Logger.data.info("Directory already exists: \(packageDirectory.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("Directory already exists: \(packageDirectory.cleanUrlPath())", category: .data)
}
// create the temp directory if it doesn't already exist
if !FileManager.default.fileExists(atPath: packageTempDirectory.path) {
if !FileManager.default.fileExists(atPath: packageTempDirectory.path(percentEncoded: false)) {
try FileManager.default.createDirectory(at: packageTempDirectory, withIntermediateDirectories: true, attributes: nil)
Logger.data.info("Created directory: \(packageTempDirectory.path, privacy: .public)")
LogUtil.infoBreadcrumb("Created directory: \(packageTempDirectory.path)", category: .data)
Logger.data.info("Created directory: \(packageTempDirectory.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("Created directory: \(packageTempDirectory.cleanUrlPath())", category: .data)
} else {
Logger.data.info("Directory already exists: \(packageTempDirectory.path, privacy: .public)")
LogUtil.infoBreadcrumb("Directory already exists: \(packageTempDirectory.path)", category: .data)
Logger.data.info("Directory already exists: \(packageTempDirectory.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("Directory already exists: \(packageTempDirectory.cleanUrlPath())", category: .data)
}
}
@ -208,7 +208,7 @@ public class PackageRepository: PackageRepo {
*/
func directoryExistsAtPath(directoryUrl: URL) -> Bool {
var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: directoryUrl.path, isDirectory: &isDirectory)
let exists = FileManager.default.fileExists(atPath: directoryUrl.path(percentEncoded: false), isDirectory: &isDirectory)
return exists && isDirectory.boolValue
}
@ -241,8 +241,8 @@ public class PackageRepository: PackageRepo {
packageMap[itemUrl] = packageSource
}
} catch let error as LoadPackageError {
Logger.data.error("package at \(itemUrl) could not be loaded: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("package at \(itemUrl) could not be loaded: \(error as NSError)", category: .data)
Logger.data.error("package at \(itemUrl.cleanUrlPath(), privacy: .public) could not be loaded: \(error as NSError, privacy: .public)")
LogUtil.errorBreadcrumb("package at \(itemUrl.cleanUrlPath()) could not be loaded: \(error as NSError)", category: .data)
}
}
}
@ -260,12 +260,12 @@ public class PackageRepository: PackageRepo {
* check the specified directory for the kmp.json file and read it if it exists
*/
func readPackageFromDirectory(packageDirectoryUrl: URL) throws -> PackageSource? {
Logger.data.info("readPackageFromDirectory from url: \(packageDirectoryUrl, privacy: .public)")
LogUtil.infoBreadcrumb("readPackageFromDirectory from url: \(packageDirectoryUrl)", category: .data)
Logger.data.info("readPackageFromDirectory from url: \(packageDirectoryUrl.cleanUrlPath(), privacy: .public)")
LogUtil.infoBreadcrumb("readPackageFromDirectory from url: \(packageDirectoryUrl.cleanUrlPath())", category: .data)
var packageSource: PackageSource? = nil
let kmpJsonFileUrl = packageDirectoryUrl.appendingPathComponent(packageFileName)
if !FileManager.default.fileExists(atPath: kmpJsonFileUrl.path) {
if !FileManager.default.fileExists(atPath: kmpJsonFileUrl.path(percentEncoded: false)) {
throw LoadPackageError.kmpJsonFileNotFound
}