-
Notifications
You must be signed in to change notification settings - Fork 3
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
Introduces BlessError
to provide detailed error explanation
#14
Conversation
Instead of just passing through Apple's (lackluster) CFError value, `BlessedError` actually runs through each requirement and provides a detailed explanation for each one that fails to be met.
@amomchilov I believe in the past this is the type of improved error handling you wanted to see added to Blessed. Would you be interested in taking a look at this and letting me know your thoughts? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty much exactly what I had in mind! <3
Sources/Blessed/BlessError.swift
Outdated
asssessHelperToolLaunchdPropertyList(label: label), // 5 & 6 | ||
asssessHelperToolInfoPropertyListAuthorizedClients(label: label, type: .bundled), // 7 & 8 - bundled | ||
asssessHelperToolInfoPropertyListAuthorizedClients(label: label, type: .installed), // 7 & 8 - installed | ||
asssessHelperToolInfoPropertyListBundleVersion(label: label), // 9 | ||
asssessAppInfoPropertyList(label: label) // 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asssessHelperToolLaunchdPropertyList(label: label), // 5 & 6 | |
asssessHelperToolInfoPropertyListAuthorizedClients(label: label, type: .bundled), // 7 & 8 - bundled | |
asssessHelperToolInfoPropertyListAuthorizedClients(label: label, type: .installed), // 7 & 8 - installed | |
asssessHelperToolInfoPropertyListBundleVersion(label: label), // 9 | |
asssessAppInfoPropertyList(label: label) // 10 | |
assessHelperToolLaunchdPropertyList(label: label), // 5 & 6 | |
assessHelperToolInfoPropertyListAuthorizedClients(label: label, type: .bundled), // 7 & 8 - bundled | |
assessHelperToolInfoPropertyListAuthorizedClients(label: label, type: .installed), // 7 & 8 - installed | |
assessHelperToolInfoPropertyListBundleVersion(label: label), // 9 | |
assessAppInfoPropertyList(label: label) // 10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh whoops(s)!
Sources/Blessed/BlessError.swift
Outdated
} | ||
|
||
// 2 & 3 | ||
fileprivate func assessHelperToolIsExecutable(label: String) -> Assessment { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kind of a nit, but if you grouped these functions into something like a HelperToolValidator
, then the method names can be shorted like isExecutable
.
That also lets you remove all these label: String
parameters, in favor of a single ivar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good idea, will do
Sources/Blessed/BlessError.swift
Outdated
if result == errSecSuccess { | ||
return .satisfied | ||
} else if result == errSecCSUnsigned { | ||
return .notSatisfied(explanation: "This application does not have a valid signature") | ||
} else { | ||
return .notDetermined(explanation: "Signature checking failed with error \(result)") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect use-case for switch
:
if result == errSecSuccess { | |
return .satisfied | |
} else if result == errSecCSUnsigned { | |
return .notSatisfied(explanation: "This application does not have a valid signature") | |
} else { | |
return .notDetermined(explanation: "Signature checking failed with error \(result)") | |
} | |
switch result { | |
case errSecSuccess: return .satisfied | |
case errSecCSUnsigned: return .notSatisfied(explanation: "This application does not have a valid signature") | |
default: return .notDetermined(explanation: "Signature checking failed with error \(result)") | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah that's a bit clearer
Sources/Blessed/BlessError.swift
Outdated
let validMagicValues: Set<UInt32> = [MH_MAGIC, MH_CIGAM, MH_MAGIC_64, MH_CIGAM_64, FAT_MAGIC, FAT_CIGAM] | ||
guard validMagicValues.contains(firstFourBytes) else { | ||
return .notSatisfied(explanation: "The bundle helper tool is not a Mach-O executable") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a built in API for checking this, without needing to specify the valid magic headers explicitly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There doesn't appear to be and that makes sense because normally there'd be different code paths needed depending on what architecture the Mach-O executable is for. There are a bunch of functions for reading parts of a Mach-O executable (which the EmbeddedPropertyList package makes use of).
Sources/Blessed/BlessError.swift
Outdated
// The helper tool's embedded launchd property list **must** have an entry with `Label` as the key and the | ||
// value **must** be the filename of the helper tool. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant comment, it's pretty explicitly spelt out below in code already
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is true, this is intentionally a word-for-word copy of the requirements for bless(...) function documentation. Each checked portion of the requirement has this throughout BlessError
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh, didn't notice. Is that explained anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not, I can add a comment somewhere before the assessments making that explicit.
Sources/Blessed/BlessError.swift
Outdated
guard let plist = try? PropertyListSerialization.propertyList(from: data, | ||
options: .mutableContainersAndLeaves, | ||
format: nil) as? NSDictionary else { | ||
return .notSatisfied(explanation: "The data embedded as the bundled helper tool's launchd property list is " + | ||
"not a valid property list") | ||
} | ||
guard let plistLabel = plist["Label"] else { | ||
return .notSatisfied(explanation: "The bundled helper tool's embedded launchd property list does not have a " + | ||
"Label key") | ||
} | ||
guard label == plistLabel as? String else { | ||
return .notSatisfied(explanation: "The bundled helper tool's launchd property list's value for the Label key " + | ||
"does not match the label for the bundled helper tool\n" + | ||
"Required value: \(label)\n" + | ||
"Actual value: \(plistLabel)") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO using multi-line strings here works quite nice. They start on a new line anyway, so you alleviate the need to word break in the middle of short sentences:
guard let plist = try? PropertyListSerialization.propertyList(from: data, | |
options: .mutableContainersAndLeaves, | |
format: nil) as? NSDictionary else { | |
return .notSatisfied(explanation: "The data embedded as the bundled helper tool's launchd property list is " + | |
"not a valid property list") | |
} | |
guard let plistLabel = plist["Label"] else { | |
return .notSatisfied(explanation: "The bundled helper tool's embedded launchd property list does not have a " + | |
"Label key") | |
} | |
guard label == plistLabel as? String else { | |
return .notSatisfied(explanation: "The bundled helper tool's launchd property list's value for the Label key " + | |
"does not match the label for the bundled helper tool\n" + | |
"Required value: \(label)\n" + | |
"Actual value: \(plistLabel)") | |
} | |
guard let plist = try? PropertyListSerialization.propertyList(from: data, | |
options: .mutableContainersAndLeaves, | |
format: nil) as? NSDictionary else { | |
return .notSatisfied(explanation: """ | |
The data embedded as the bundled helper tool's launchd property list is not a valid property list | |
""") | |
} | |
guard let plistLabel = plist["Label"] else { | |
return .notSatisfied(explanation: """ | |
The bundled helper tool's embedded launchd property list is missing a value for the key "Label" | |
""") | |
} | |
guard label == plistLabel as? String else { | |
return .notSatisfied(explanation: """ | |
The bundled helper tool's launchd property list's value for the "Label" key | |
does not match the label for the bundled helper tool. | |
Required value: \(label) | |
Actual value: \(plistLabel) | |
""") | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applies nicely to a few other places too
Sources/Blessed/BlessError.swift
Outdated
for requirement in authorizedClientRequirements { | ||
let evaluation = try? (try? Parser.parse(requirement: requirement))?.evaluateForCode(code) | ||
explanation += "\n\(evaluation?.prettyDescription ?? "Failed to evaluate requirement.")" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be simplified to a flatMap
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could use a flatMap
, but I don't see how it could be made simpler because the result would then be an array which then would need to be turned into a single string via joined
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reduce
works nicely though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you get back a [Character]
. Joining those is just a matter of running them through the string initializer, e.g.
String(["line 1\n", "line 2\n"].flatMap { "foo - " + $0 })
reduce
also works, but it has n^2 time performance, so just be wary if you ever use this pattern in the future for large strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this specific case I expect n to most commonly be 1, occasionally 2 or perhaps 3.
More broadly though, why would it be O(n^2) as opposed to O(n)? It looks like it performs n iterations and allows for combining the result of one iteration with the next one. Is your comment about the cost of appending a string to another string? If so I'd expect that'd be a factor of how long the string is, not the number of elements in the collection for which reduce
was called on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is your comment about the cost of appending a string to another string?
Yep
If so I'd expect that'd be a factor of how long the string is, not the number of elements in the collection for which reduce was called on.
It's a function of both the inputs strings' sizes, and their quantity. Larger strings (early on) means there's more to copy on each string reallocation, and a higher quantity means that more copies are necessary.
Definitely not a concern here, but just a warning for something to watch out for.
Actually, this makes me wonder: Are strings "right-sized" for the immediate need, or do they grow with extra capacity (e.g. capacity *= 1.5
on every realloc) similar to an Array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update, did some digging, yes String has slack: https://github.com/apple/swift/blob/0c67ce64874d83b2d4f8d73b899ee58f2a75527f/stdlib/public/core/StringStorage.swift#L457-L467
I guess this, plus the ability to mutate in-place (when uniquely referenced) makes sense why there isn't a StringBuilder
like in Java (where appending even a single letter to a string would always require a full copy of the first part).
// The numbers in front of each function corresponds to the requirements described in LaunchdManager.bless(...) | ||
|
||
|
||
fileprivate struct HelperToolAssessor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This turned out great!
And some other minor misc improvements
Instead of just passing through Apple's (lackluster) CFError value,
BlessedError
actually runs through each requirement and provides a detailed explanation for each one that fails to be met. This is done in part by adding dependencies on theEmbeddedPropertyList
andRequired
packages.