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

Introduces BlessError to provide detailed error explanation #14

Merged
merged 4 commits into from
Jun 20, 2022

Conversation

jakaplan
Copy link
Contributor

@jakaplan jakaplan commented Jun 16, 2022

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 the EmbeddedPropertyList and Required packages.

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.
@jakaplan
Copy link
Contributor Author

@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?

Copy link

@amomchilov amomchilov left a 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

Comment on lines 30 to 34
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh whoops(s)!

}

// 2 & 3
fileprivate func assessHelperToolIsExecutable(label: String) -> Assessment {

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

Copy link
Contributor Author

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

Comment on lines 116 to 122
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)")
}

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:

Suggested change
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)")
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise below

Copy link
Contributor Author

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

Comment on lines 147 to 149
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")

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?

Copy link
Contributor Author

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).

Comment on lines 187 to 188
// 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.

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

Copy link
Contributor Author

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.

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?

Copy link
Contributor Author

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.

Comment on lines 189 to 204
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)")
}

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:

Suggested change
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)
""")
}

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

Comment on lines 280 to 283
for requirement in authorizedClientRequirements {
let evaluation = try? (try? Parser.parse(requirement: requirement))?.evaluateForCode(code)
explanation += "\n\(evaluation?.prettyDescription ?? "Failed to evaluate requirement.")"
}

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?

Copy link
Contributor Author

@jakaplan jakaplan Jun 17, 2022

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce works nicely though

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

Copy link
Contributor Author

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.

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?

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 {

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
@jakaplan jakaplan merged commit 50e98c0 into main Jun 20, 2022
@jakaplan jakaplan deleted the blessed-error branch June 21, 2022 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants