| #!/usr/bin/env ruby |
| |
| # Copyright (C) 2020 Igalia S. L. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| # THE POSSIBILITY OF SUCH DAMAGE. |
| |
| # Resolve conflicts in `.file` directives in assembler files. Those |
| # may result from inline asm statements which themselves have `.file` |
| # directives. As the inline asm has no idea what the next free file |
| # number is, it'll generally have its own numbering. To get things to |
| # work we have to |
| # 1. remap conflicting file numbers |
| # 2. change `.loc` directives to reference the appropriate file number. |
| # |
| # To be able to do that, we need some concept of "scope", i.e. which |
| # set of files a given `.loc` directive refers to. We get that by |
| # tracking the #APP/#NOAPP directives that the compiler emits when it |
| # switch to/from inline asm. |
| # |
| # In effect, we convert |
| # .file 1 "foo" |
| # #APP |
| # .file 1 "bar" |
| # .file 2 "foo" |
| # .loc 1, X |
| # #NOAPP |
| # .loc 1, Y |
| # to |
| # .file 1 "foo" |
| # #APP |
| # .file 2 "bar" |
| # .file 1 "foo" |
| # .loc 2, X |
| # #NOAPP |
| # .loc 1, Y |
| |
| require 'pathname' |
| require 'stringio' |
| require 'strscan' |
| |
| ParseResultSuccess = Struct.new(:str) |
| ParseResultError = Struct.new(:error) |
| |
| # Parses whatever follows a .file assembler directive |
| class FileDirectiveArgScanner |
| def initialize(s) |
| @s = StringScanner.new(s) |
| end |
| def parse |
| @s.skip(/\s*/) |
| |
| # We require at least one string literal |
| ret1 = parse_string_literal |
| if ret1.respond_to?(:error) |
| return ret1 |
| end |
| |
| @s.skip(/\s*/) |
| if @s.eos? |
| return ParseResultSuccess.new(Pathname.new(ret1.str).cleanpath.to_s) |
| end |
| # If anything follows, it needs to be a string literal |
| ret2 = parse_string_literal |
| if ret2.respond_to?(:error) |
| return ret2 |
| end |
| @s.skip(/\s*/) |
| if not @s.eos? |
| md5 = parse_md5 |
| if md5.respond_to?(:error) |
| return ParseResultError.new("Expected end of line or md5, not `#{@s.rest}`") |
| end |
| end |
| @s.skip(/\s*/) |
| if not @s.eos? |
| return ParseResultError.new("Expected end of line, not `#{@s.rest}`") |
| end |
| filepath = Pathname.new(ret2.str) |
| if not filepath.absolute? |
| filepath = Pathname.new(ret1.str) / ret2.str |
| end |
| return ParseResultSuccess.new(filepath.cleanpath.to_s) |
| end |
| def parse_string_literal |
| if @s.scan(/"/).nil? |
| err = "Expected string literal at `#{@s.string}` (pos #{@s.pos})" |
| return ParseResultError.new(err) |
| end |
| parse_until_end_of_string_literal |
| end |
| def parse_until_end_of_string_literal |
| start_pos = @s.pos |
| while true |
| # Search for our special characters |
| @s.skip(/[^"\\]+/) |
| if @s.scan(/\\/) |
| if @s.scan(/\\/) |
| # When we see '\\', consume both characters so that the |
| # second '\' will not be treated as an escape char. |
| next |
| elsif @s.scan(/"/) |
| # For '\"', consume both characters so that the '"' will not |
| # terminate the string. |
| next |
| end |
| next |
| elsif @s.scan(/"/) |
| # '"' without a preceeding '\'; terminate the literal. |
| # We're already past the '"', so the literal ends at -2 |
| # characters. |
| return ParseResultSuccess.new(@s.string[start_pos..(@s.pos - 2)]) |
| elsif @s.eos? |
| err = "Unterminated string literal (starting at pos #{start_pos} in #{@s.string}" |
| return ParseResultError.new(err) |
| end |
| raise "Internal error (#{@s.inspect})" |
| end |
| end |
| def parse_md5 |
| md5 = @s.scan(/md5\s+(0x)?\h+/) |
| if md5.nil? |
| ParseResultError.new("Could not parse md5 at pos #{@s.pos} in #{@s.string}") |
| else |
| ParseResultSuccess.new(md5) |
| end |
| end |
| end |
| |
| def test(outf, str, res) |
| pr = FileDirectiveArgScanner.new(str).parse |
| if res.is_a?(Array) |
| if pr.respond_to?(:error) |
| outf.puts("Parse result is `#{pr.error}` but expected #{res}") |
| return false |
| end |
| if pr.str != res[0] |
| outf.puts("Parsed path `#{pr.str}` but expected `#{res[0]}`") |
| return false |
| end |
| return true |
| elsif res.is_a?(String) |
| if pr.respond_to?(:error) |
| if not pr.error.downcase.include?(res.downcase) |
| err = "Error message `#{pr.error}` does not include expected substring `#{res}`" |
| outf.puts(err) |
| return false |
| end |
| return true |
| end |
| outf.puts("Expected error (#{res}) but got successful parse #{pr.str}") |
| return false |
| else |
| raise "Internal error #{res.class}" |
| end |
| end |
| |
| def selftest |
| nr_succeeded = 0 |
| tests = [ |
| # simple string |
| ['"foo/bar"', ["foo/bar"]], |
| |
| ['"foo', "Unterminated string literal"], |
| |
| ['"foo\"', "Unterminated string literal"], |
| |
| # "foo\" |
| ["\"foo\\\"", "Unterminated string literal"], |
| |
| # "foo\\" |
| ["\"foo\\\\\"", ["foo\x5c\x5c"]], |
| |
| # Can escape '"' |
| ['"foo\"bar"', ['foo\"bar']], |
| |
| # Can parse relative |
| ['"foo/bar"', ['foo/bar']], |
| |
| # Can parse absolute |
| ['"/bar"', ['/bar']], |
| |
| # Can parse absolute with two components |
| ['"/bar/baz"', ['/bar/baz']], |
| |
| # Can detect stray token |
| ['"foo" bar', "Expected string literal"], |
| |
| # Can detect stray token without whitespace |
| ['"foo"bar', "Expected string literal"], |
| |
| # Can parse two string literals |
| ['"working_directory" "path"', ['working_directory/path']], |
| |
| # Will not concatenate the working directory to an absolute path |
| ['"working_directory" "/path"', ['/path']], |
| |
| # Will only accept up to 2 string literals |
| ['"first" "second" "third"', "Expected end of line"], |
| |
| # Can detect stray token after 2nd string literal |
| ['"foo" "bar" baz', "Expected end of line"], |
| |
| # Can detect stray token after 2nd string literal without whitespace |
| ['"foo" "bar"baz', "Expected end of line"], |
| |
| # Can detect unterminated 3rd string literal |
| ['"foo" "bar" "baz', "Expected end of line"], |
| |
| # Can parse md5 |
| ['"foo" "bar" md5 0xabcde0123456789', ['foo/bar']], |
| |
| # Can parse md5 without 0x prefix |
| ['"foo" "bar" md5 abcde0123456789', ['foo/bar']] |
| |
| ] |
| outf = StringIO.new("") |
| tests.each { |str, res| |
| if test(outf, str, res) |
| nr_succeeded += 1 |
| end |
| } |
| if nr_succeeded != tests.size |
| $stderr.puts(outf.string) |
| $stderr.puts("Some self tests failed #{nr_succeeded}/#{tests.size}") |
| exit(3) |
| end |
| end |
| |
| # Keep track of which fileno is assigned to which file path. We call |
| # the fileno a 'slot'. |
| class FileSlotTracker |
| @@next_free = 1 # Next free global slot |
| @@paths = {} # Maps path -> global slot |
| def initialize(parent) |
| @parent = parent |
| # This maps from our own local slots (i.e. within an APP section) |
| # to a global slot. |
| @slot_map = {} |
| end |
| # We make sure that .file directives that specify the same path are |
| # dedup'd, i.e. if we see |
| # .file N "/path/to/include.h" |
| # ... |
| # .file M "/path/to/include.h" |
| # then the "local" N, M slots will be mapped to the same "global" slot. |
| def register_path(path, slot) |
| curr_slot = @@paths[path] |
| if curr_slot.nil? |
| # We haven't seen this file before |
| if slot <= @@next_free |
| # Desired file slot either clashes with an existing one, or is |
| # the next one to be allocated. In either case, assign the |
| # next free slot. |
| assign_next_slot(path) |
| else |
| # Don't allow slot gaps. Technically we could handle them, but |
| # they should never happen; bail now rather than papering over |
| # an earlier error. |
| $stderr.puts("File wants slot #{slot} but only seen #{@@next_free} so far") |
| exit(2) |
| end |
| else |
| # We've already assigned a slot for this file. |
| end |
| @slot_map[slot] = @@paths[path] |
| if @slot_map[slot].nil? |
| raise "Didn't map local slot #{slot}" |
| end |
| end |
| # Return global slot for path |
| def slot_for_path(path) |
| return @@paths[path] |
| end |
| # Return global slot that will replace the local slot |
| def remap_slot(slot) |
| ret = nil |
| if @slot_map.size > 0 |
| # If the current NO_APP segment has defined a .file, only look |
| # in the current FileSlotTracker. This is the case for a |
| # top-level inline asm statement. |
| ret = @slot_map[slot] |
| elsif not @parent.nil? |
| # If the current NO_APP segment has not defined a .file, clearly |
| # all .loc directives refer to files defined in the APP |
| # part. This is the case for non-top-level inline asm |
| # statements. |
| ret = @parent.remap_slot(slot) |
| end |
| if ret.nil? |
| raise "No global slot for #{slot}" |
| end |
| ret |
| end |
| private |
| def assign_next_slot(path) |
| @@paths[path] = @@next_free |
| @@next_free += 1 |
| end |
| end |
| |
| # Return sequential lines, while keeping track of whether we're in an |
| # #APP or #NOAPP section. |
| class AsmReader |
| attr_reader :in_app |
| attr_accessor :app_to_noapp, :noapp_to_app |
| def initialize(f) |
| @f = f |
| @f.rewind |
| @linenr = 0 |
| @in_app = false |
| @last_switch = "start of file" # For error messages |
| end |
| def next_line |
| while true |
| l = @f.gets |
| if l.nil? |
| return l |
| end |
| @linenr += 1 |
| if /^#\s*APP\s*/.match(l) |
| if @in_app |
| raise "#APP on line #{@linenr} but already in #APP (#{@last_switch})" |
| end |
| @in_app = true |
| @last_switch = @linenr |
| if @noapp_to_app |
| @noapp_to_app.call() |
| end |
| end |
| if /^#\s*NO_APP\s*/.match(l) |
| if not @in_app |
| raise "#NOAPP on line #{@linenr} but was not in #APP (last swich at #{@last_switch})" |
| end |
| @in_app = false |
| @last_switch = @linenr |
| if @app_to_noapp |
| @app_to_noapp.call() |
| end |
| end |
| return l |
| end |
| end |
| end |
| |
| class FileConflictResolver |
| @@file_re = /^\s*[.]file\s+(?<slot>\d+)\s+(?<rest>.*)$/ |
| @@loc_re = /^(?<white1>\s*)[.]loc(?<white2>\s+)(?<slot>\d+)(?<rest>\s+\d+.*)$/ |
| def initialize(inf, outf) |
| @outf = outf |
| @trackers = [FileSlotTracker.new(nil)] |
| @asm_reader = AsmReader.new(inf) |
| # When we enter an #APP section (i.e. asm emitted from an expanded |
| # inline asm statement), create a new file tracker, in effect |
| # processing the .file and .loc directives in a new "namespace" |
| # (as far as the file numbers are concerned). This is an array, |
| # but in practice the size will either be 1 (NOAPP) or 2 (APP). |
| @asm_reader.app_to_noapp = Proc.new { || |
| @trackers.pop |
| } |
| @asm_reader.noapp_to_app = Proc.new { || |
| @trackers.push(FileSlotTracker.new(@trackers[-1])) |
| } |
| end |
| def run |
| while true |
| l = @asm_reader.next_line |
| break unless l |
| |
| md = @@file_re.match(l) |
| if md |
| file_directive(md) |
| next |
| end |
| md = @@loc_re.match(l) |
| if md |
| loc_directive(md) |
| next |
| end |
| @outf.write(l) |
| next |
| end |
| end |
| def loc_directive(md) |
| tracker = @trackers.last |
| slot = tracker.remap_slot(md[:slot].to_i) |
| @outf.puts("#{md[:white1]}.loc#{md[:white2]}#{slot}#{md[:rest]}") |
| end |
| def file_directive(md) |
| slot = md[:slot].to_i |
| tracker = @trackers.last |
| |
| pr = FileDirectiveArgScanner.new(md[:rest]).parse |
| if pr.respond_to?(:error) |
| $stderr.puts("Error parsing path argument to .file directive: #{pr.error}") |
| exit(2) |
| end |
| |
| path = pr.str |
| |
| if slot != 0 |
| tracker.register_path(path, slot) |
| slot = tracker.slot_for_path(path) |
| end |
| @outf.puts("\t.file\t#{slot} #{md[:rest]}") |
| end |
| end |
| |
| # First, make sure our tests still pass. This only takes a fraction of |
| # our runtime and ensures the tests will get run by anyone trying out |
| # changes to this file. |
| selftest |
| |
| if ARGV.size != 2 |
| $stderr.puts("Usage: #{$0} input output") |
| exit(2) |
| end |
| |
| inpath, outpath = ARGV.collect { |n| Pathname.new(n) } |
| |
| if not inpath.file? |
| $stderr.puts("Not a regular file: `#{inpath}`") |
| exit(2) |
| end |
| |
| if inpath.extname.upcase != ".S" |
| $stderr.puts("warning: file `#{inpath}` doesn't have a `.s` or `.S` extension. Going on anyway...") |
| end |
| |
| File.open(inpath, "r") { |inf| |
| File.open(outpath, "w") { |outf| |
| FileConflictResolver.new(inf, outf).run |
| } |
| } |