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
Allow suppression of exceptions with strict: false #55
Conversation
Once this is in, I'll push a release and open a brew PR to update the vendored version/calls to modification methods. |
I think we might have a misunderstanding what the strict vs. non-strict behavior should be. Or maybe I'm misreading your code. I'm assuming a fat file with more than one Mach-O slice in the following, but thin files (regular Mach-O files) can be treated as if they were fat files with a single slice. Here's what I had in mind:
From reading the diff in this PR I got the impression you're also treating case (3) as success in non-strict mode, which is not something I'm willing to allow, as this would open the floodgates and will silently ignore too many problems. I think the easiest way to achieve what I had in mind (aside from adding
It's not strictly necessary, but would be nice to have #49 in the release, too. Otherwise: Agreed! |
You're reading correctly, setting
This makes sense, although I don't generally like using exceptions as control flow. I think it might be easier (and [...]
successful = machos.count { |m| m.dylib_id == new_id }
raise AnErrorExpressiveOfCompleteFailure if successful == 0
synchronize_raw_data Where
Sounds good to me! |
f7dae25
to
6882bc5
Compare
@UniqMartin Any thoughts on my counterproposal for the case when all slices fail? I'd like to push this PR along to keep us on track with the schedule 😄 |
Apologies for the delay! Since we're on the same page regarding the desired behavior, please next time just move ahead and make the necessary adjustments and implement them how you think it should be done, unless doing so would mean an excessive amount of work. It's always easier to discuss actual code and it's quite possible that you'll either notice design flaws in your idea while implementing it or the finished code turns out being superior to what I had in mind. In any case and unless you already have done so, please use the time until you hear back from me to construct tests that check for the desired behavior, e.g. for this PR fat binaries with inconsistent Mach-O slices and test code that works with them.
In general, I tend to agree here. The idea that was backing this suggestion would be that this would allow for richer error handling, where the exception raised in the top-level modification method that ensures the modification could be applied to any/all slices could more easily point out what went wrong and which Mach-O slice was responsible for this.
I think this idea works fairly well for the presented case of |
Yeah, that's true. Doing this by filtering exceptions sounds good to me with that in mind. Do you have a particular structure in mind? Catching exceptions and counting them with an accumulator is what immediately comes to mind, but that doesn't feel very ruby-esque.
I'll get on this. I think there's a |
Never mind, looks like |
Roughly, this is what I had in mind: errors = []
machos.each_with_index do |macho, index|
begin
macho.‹operation›
rescue AppropriateBaseClass => e
errors << [index, e]
end
end
strict = options.fetch(:strict, false)
failed = strict ? !errors.empty? : errors.size == machos.size
if failed
macho_index, cause = errors.first
raise MachOSliceModificationError.new(strict, macho_index, cause)
end Here, It obviously doesn't make sense to repeat all this in every one of the methods, but the logic could be easily encapsulated, such that, e.g., def delete_rpath(path, options = {})
each_machos_with_check(options) do |macho|
macho.delete_rpath(path)
end
synchronize_raw_data
end Of course all those newly invented names are up for debate (I didn't think too long about them and as we all know naming things is hard). |
Since we already have a (flat)
I think something like this would be ideal (perhaps just |
Sounds all good to me! Let's get this idea implemented and then we can discuss the finer details with actual code in front of us. (I doubt we'll have to overthrow the entire design again.) |
After thinking about this, I think that it makes more sense to just rescue the toplevel |
6882bc5
to
84a9cfe
Compare
(I have to admit that I failed to fully parse this comment, thus forgive me if my comment is misguided.) This sounds like a dangerous path to take. We definitely don't want to rescue and silence all exceptions if they only occur for a single Mach-O slice out of possibly many slices. An example: One slice cannot be modified because there's insufficient space in the Mach-O header padding, while all other slices have sufficient padding and thus the update succeeds there. This should be causing an error even in non-strict mode. |
You understood it correctly! I wasn't thinking, we definitely don't want to silence those types of errors. |
strictness = strict ? "strict" : "nonstrict" | ||
inner_error = "#{cause.class}: #{cause}" | ||
|
||
super "#{inner_error} during #{strictness} modification at slice #{index}" |
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.
“[…] modification of slice […]”?
098fb05
to
35baf19
Compare
Using an encapsulating exception like macho.dylib_id = "x" * 4096 # HeaderPadError
fat.dylib_id = "x" * 4096 # MachOSliceModificationError That's not a dealbreaker for me (since both are children of Besides that (very minor) nit, what do you think of renaming |
I can relate to this concern. Though you managed to pick an example that isn't related to the problem. 😉 I thought we had agreed that a
We could also equip Now that this popped into my head, I think this is a superior approach to the error handling (or at least to how the exceptions are organized and re-raised). What do you think?
I guess this question is somewhat irrelevant, if you agree on the above. But my thinking was that |
@@ -3,6 +3,10 @@ module MachO | |||
class MachOError < RuntimeError | |||
end | |||
|
|||
# A generic Mach-O error during file modification. | |||
class MachOModificationError < MachOError |
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.
Let's be super-explicit here. I think this should be something like “A generic Mach-O error during file modification, that is non-fatal and might be suppressed if operating in non-strict mode.” (I'm not particularly happy with this wording, but I hope the message gets across. Please feel free to phrase this better!)
We could also consider a name different from MachOModificationError
that better reflects the nature of these exceptions.
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.
We could also consider a name different from MachOModificationError that better reflects the nature of these exceptions.
This might get us uncomfortably close to checked and unchecked exceptions a la Java, but how about NonFatalModificationError
(and FatalModificationError
for things like HeaderPadError
)? (Update: Got rid of the Mach-O prefixes...)
Agreed on the documentation 👍
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'm a little confused about how we're using "fatal" here, actually: do we mean "fatal" as in "halts the program" or as in "would do something very bad [to the Mach-O] if it were allowed to continue"?
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.
Sorry for the confusion! I'm not entirely happy with the “fatal” term either but couldn't think of a better term. In this context I was using it to distinguish between modification errors that should always be raised (header pad error being one example) and modification errors that could be absorbed by our front-end API (e.g. FatFile#delete_rpath
) in non-strict mode (e.g. RPATH deletion succeeds on one Mach-O slice but fails on another).
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.
No worries! I was also confusing myself my reusing the term in the exceptions. How do you feel about CheckedModificationError
and UncheckedModificationError
instead? That gets us away from the connotation of program halting and more accurately conveys the fact that we're checking and filtering subclasses of the particular error.
if old_lc.nil? | ||
return unless options.fetch(:strict, true) | ||
raise RpathUnknownError.new(old_path) | ||
end |
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 change needs to be reverted. The modification methods in MachOFile
need to always raise an exception, independently of options
(because options
currently only matters if multiple Mach-O slices are involved).
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.
You brought back the original checks further up in this method but still left this new code intact?
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.
Oops...
35baf19
to
444c333
Compare
Could you describe the structure you have in mind? Sorry, I'm having a little trouble visualizing it 😅 |
@@ -3,6 +3,14 @@ module MachO | |||
class MachOError < RuntimeError | |||
end | |||
|
|||
# A generic and non-fatal Mach-O error during file modification. |
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 we can drop generic
from the description. It doesn't seem to add any value.
444c333
to
7fb42da
Compare
Pulling this out from #55 (comment) so it's not completely hidden (and lost):
That's a nice suggestion; I think that's terminology we could borrow. I went looking for something similar in the Ruby world, but I didn't find anything. Another idea I just came up with is to use
Does this look reasonable?
Will do. Give me a few moments to prepare some demo code. |
Code (changes on top of this branch): UniqMartin@cfa5e91 in my Preparations: $ cp test/bin/i386/hello.bin hello-i386
$ cp test/bin/x86_64/hello.bin hello-x86_64
$ lipo -create -output hello-consistent hello-i386 hello-x86_64
$ install_name_tool -add_rpath /mesa/special hello-x86_64
$ lipo -create -output hello-inconsistent hello-i386 hello-x86_64
$ ruby -Ilib -rmacho -e 'puts MachO.open(ARGV.shift).machos.map(&:rpaths).inspect' hello-consistent
[["made_up_path"], ["made_up_path"]]
$ ruby -Ilib -rmacho -e 'puts MachO.open(ARGV.shift).machos.map(&:rpaths).inspect' hello-inconsistent
[["made_up_path"], ["made_up_path", "/mesa/special"]] Tests (the important difference and the point of this PR is the treatment of $ # Strict mode:
$ add-rpath() { ruby -Ilib -rmacho -e 'MachO.open(ARGV.shift).add_rpath(*ARGV, :strict => true) rescue puts $!.inspect' "$@"; }
$ add-rpath hello-inconsistent /mesa/special
#<MachO::RpathExistsError: While modifying Mach-O slice 1: /mesa/special already exists>
$ # … no change for consistent fat binaries:
$ add-rpath hello-consistent made_up_path
#<MachO::RpathExistsError: While modifying Mach-O slice 0: made_up_path already exists>
$ add-rpath hello-consistent /mesa/special
$ # … no change for non-fat binaries:
$ add-rpath hello-i386 made_up_path
#<MachO::RpathExistsError: made_up_path already exists>
$ add-rpath hello-i386 /mesa/special
$ # Non-strict mode:
$ add-rpath() { ruby -Ilib -rmacho -e 'MachO.open(ARGV.shift).add_rpath(*ARGV, :strict => false) rescue puts $!.inspect' "$@"; }
$ add-rpath hello-inconsistent /mesa/special
$ # … no change for consistent fat binaries:
$ add-rpath hello-consistent made_up_path
#<MachO::RpathExistsError: While modifying Mach-O slice 0: made_up_path already exists>
$ add-rpath hello-consistent /mesa/special
$ # … no change for non-fat binaries:
$ add-rpath hello-i386 made_up_path
#<MachO::RpathExistsError: made_up_path already exists>
$ add-rpath hello-i386 /mesa/special |
Yeah, I think
Yep! |
This looks great! Re-raising the exception immediately with the added context feels much more natural to me. I cherry-picked your commit right on top of this branch - do you know how to preserve authorship (if possible) during a squash? |
I guess there must be a more elegant solution, but this works: $ git rebase -i HEAD~2 # squash as usual
$ git commit --no-edit --amend --author="$(git show -s --format='%an <%ae>' origin/lc-modification-strictness)" # steal authorship information from commit authored by me |
…tions. Resolves the problem of valid discrepancies between individual Mach-Os in a fat file causing exceptions during modifications. Distinguish between recoverable and unrecoverable modification errors. Closes #50.
d16c04e
to
d395c05
Compare
I feel a bit bad stealing your commit credit as you've done the bulk of the work, though. 😳 |
Don't worry about it; although we could probably do something like Either way, I still show up as the PR's author and committer, so I'm fine with it 😄 |
Then please go ahead and squash-merge this yourself, assuming you're happy with the PR. (I think it's ready.) Otherwise if I do this myself, you'll be robbed of your committer credit, too. |
Done! 🎉 |
Awesome! 🎉 Thanks for surviving through my reviews and constant complaints. 😂 |
Resolves the problem of valid discrepancies between individual Mach-Os in a fat
file causing exceptions during modifications.
ref #50
cc @UniqMartin