#!/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+(?\d+)\s+(?.*)$/ @@loc_re = /^(?\s*)[.]loc(?\s+)(?\d+)(?\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 } }