blob: fe315c28fbe0425f6de1aff1894dcf62e450a778 [file] [log] [blame]
#!/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
}
}