rubycc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +56 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +52 -0
- data/README.md +208 -0
- data/data/README.md +117 -0
- data/data/r10_corpus_scan.json +4082 -0
- data/data/r10_manual_classification.json +5342 -0
- data/data/r10_verification_m4a.json +1531 -0
- data/data/r10_verification_m4b.json +1818 -0
- data/data/r10_verification_m4c.json +1489 -0
- data/data/r10_verification_m4d.json +318 -0
- data/data/r10_verification_rbs.json +192 -0
- data/data/verified_gems.json +397 -0
- data/exe/rmake +16 -0
- data/exe/rubycc +9 -0
- data/exe/rubycc-ar +114 -0
- data/exe/rubycc-doctor +14 -0
- data/exe/rubycc-pkgconf +8 -0
- data/include/float.h +87 -0
- data/include/iso646.h +18 -0
- data/include/libc/alloca.h +18 -0
- data/include/libc/arpa/inet.h +61 -0
- data/include/libc/assert.h +43 -0
- data/include/libc/dirent.h +72 -0
- data/include/libc/dlfcn.h +58 -0
- data/include/libc/features.h +205 -0
- data/include/libc/glibc/aarch64/ctype.h +108 -0
- data/include/libc/glibc/aarch64/endian.h +54 -0
- data/include/libc/glibc/aarch64/errno.h +154 -0
- data/include/libc/glibc/aarch64/fcntl.h +144 -0
- data/include/libc/glibc/aarch64/inttypes.h +182 -0
- data/include/libc/glibc/aarch64/limits.h +63 -0
- data/include/libc/glibc/aarch64/pthread.h +131 -0
- data/include/libc/glibc/aarch64/setjmp.h +84 -0
- data/include/libc/glibc/aarch64/stdint.h +174 -0
- data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
- data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
- data/include/libc/glibc/aarch64/sys/select.h +75 -0
- data/include/libc/glibc/aarch64/sys/stat.h +136 -0
- data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
- data/include/libc/glibc/aarch64/sys/time.h +74 -0
- data/include/libc/glibc/aarch64/sys/types.h +127 -0
- data/include/libc/glibc/aarch64/time.h +113 -0
- data/include/libc/glibc/x86_64/ctype.h +108 -0
- data/include/libc/glibc/x86_64/endian.h +54 -0
- data/include/libc/glibc/x86_64/errno.h +154 -0
- data/include/libc/glibc/x86_64/fcntl.h +134 -0
- data/include/libc/glibc/x86_64/inttypes.h +182 -0
- data/include/libc/glibc/x86_64/limits.h +61 -0
- data/include/libc/glibc/x86_64/pthread.h +111 -0
- data/include/libc/glibc/x86_64/setjmp.h +82 -0
- data/include/libc/glibc/x86_64/stdint.h +163 -0
- data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
- data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
- data/include/libc/glibc/x86_64/sys/select.h +75 -0
- data/include/libc/glibc/x86_64/sys/stat.h +130 -0
- data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
- data/include/libc/glibc/x86_64/sys/time.h +74 -0
- data/include/libc/glibc/x86_64/sys/types.h +123 -0
- data/include/libc/glibc/x86_64/time.h +113 -0
- data/include/libc/grp.h +50 -0
- data/include/libc/langinfo.h +126 -0
- data/include/libc/link.h +18 -0
- data/include/libc/locale.h +83 -0
- data/include/libc/math.h +191 -0
- data/include/libc/netinet/in.h +126 -0
- data/include/libc/netinet/tcp.h +44 -0
- data/include/libc/poll.h +46 -0
- data/include/libc/pwd.h +58 -0
- data/include/libc/regex.h +53 -0
- data/include/libc/sched.h +35 -0
- data/include/libc/signal.h +204 -0
- data/include/libc/stdio.h +157 -0
- data/include/libc/stdlib.h +92 -0
- data/include/libc/string.h +74 -0
- data/include/libc/strings.h +25 -0
- data/include/libc/sys/cdefs.h +126 -0
- data/include/libc/sys/inotify.h +111 -0
- data/include/libc/sys/ioctl.h +36 -0
- data/include/libc/sys/mman.h +65 -0
- data/include/libc/sys/param.h +41 -0
- data/include/libc/sys/resource.h +109 -0
- data/include/libc/sys/socket.h +167 -0
- data/include/libc/sys/statfs.h +82 -0
- data/include/libc/sys/timerfd.h +55 -0
- data/include/libc/sys/uio.h +40 -0
- data/include/libc/sys/un.h +25 -0
- data/include/libc/sys/utsname.h +35 -0
- data/include/libc/sys/wait.h +135 -0
- data/include/libc/termios.h +179 -0
- data/include/libc/unistd.h +194 -0
- data/include/stdalign.h +16 -0
- data/include/stdarg.h +31 -0
- data/include/stdatomic.h +158 -0
- data/include/stdbool.h +15 -0
- data/include/stdckdint.h +28 -0
- data/include/stddef.h +60 -0
- data/include/stdnoreturn.h +18 -0
- data/include/x86intrin.h +16 -0
- data/lib/rubycc/backend/aarch64.rb +1724 -0
- data/lib/rubycc/backend/x86_64.rb +1369 -0
- data/lib/rubycc/compile_error.rb +36 -0
- data/lib/rubycc/compiler.rb +305 -0
- data/lib/rubycc/doctor/builder.rb +151 -0
- data/lib/rubycc/doctor/cli.rb +190 -0
- data/lib/rubycc/doctor/fetcher.rb +84 -0
- data/lib/rubycc/doctor/gemfile.rb +115 -0
- data/lib/rubycc/doctor/verified_gems.rb +114 -0
- data/lib/rubycc/doctor.rb +16 -0
- data/lib/rubycc/driver.rb +463 -0
- data/lib/rubycc/front/ast.rb +528 -0
- data/lib/rubycc/front/constant_evaluator.rb +631 -0
- data/lib/rubycc/front/initializer_resolver.rb +592 -0
- data/lib/rubycc/front/lexeme_reader.rb +460 -0
- data/lib/rubycc/front/lexer.rb +232 -0
- data/lib/rubycc/front/parser.rb +4122 -0
- data/lib/rubycc/front/token.rb +49 -0
- data/lib/rubycc/ir/call_convention.rb +486 -0
- data/lib/rubycc/ir/generator.rb +6036 -0
- data/lib/rubycc/ir/ir.rb +417 -0
- data/lib/rubycc/link/compat_runtime.rb +89 -0
- data/lib/rubycc/link/errors.rb +13 -0
- data/lib/rubycc/link/executable_linker.rb +405 -0
- data/lib/rubycc/link/library_resolver.rb +437 -0
- data/lib/rubycc/link/partial_linker.rb +546 -0
- data/lib/rubycc/link/shared_linker.rb +1732 -0
- data/lib/rubycc/mkmf_shim.rb +83 -0
- data/lib/rubycc/objfile/ar_archive.rb +437 -0
- data/lib/rubycc/objfile/elf_reader.rb +646 -0
- data/lib/rubycc/objfile/elf_writer.rb +891 -0
- data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
- data/lib/rubycc/pkgconf/cli.rb +139 -0
- data/lib/rubycc/pkgconf/errors.rb +39 -0
- data/lib/rubycc/pkgconf/model.rb +28 -0
- data/lib/rubycc/pkgconf/parser.rb +109 -0
- data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
- data/lib/rubycc/pkgconf/resolver.rb +70 -0
- data/lib/rubycc/pkgconf/search_path.rb +38 -0
- data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
- data/lib/rubycc/preprocess/constant_expression.rb +219 -0
- data/lib/rubycc/preprocess/glibc_version.rb +124 -0
- data/lib/rubycc/preprocess/pp_token.rb +62 -0
- data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
- data/lib/rubycc/preprocess/scanner.rb +290 -0
- data/lib/rubycc/preprocess/token_converter.rb +157 -0
- data/lib/rubycc/rmake/cli.rb +143 -0
- data/lib/rubycc/rmake/errors.rb +71 -0
- data/lib/rubycc/rmake/executor.rb +818 -0
- data/lib/rubycc/rmake/expander.rb +251 -0
- data/lib/rubycc/rmake/makefile.rb +352 -0
- data/lib/rubycc/rmake/model.rb +137 -0
- data/lib/rubycc/rmake/parser.rb +226 -0
- data/lib/rubycc/rmake/rmake.rb +15 -0
- data/lib/rubycc/type.rb +1236 -0
- data/lib/rubycc/version.rb +5 -0
- data/lib/rubycc.rb +36 -0
- data/lib/rubygems_plugin.rb +102 -0
- metadata +219 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
# The mkmf integration shim: requiring this file (before `require "mkmf"`)
|
|
6
|
+
# reroutes every conftest command mkmf issues — compile, link, preprocess — to
|
|
7
|
+
# rubycc, without patching mkmf itself. mkmf builds its probe commands out of a
|
|
8
|
+
# handful of RbConfig keys (`$(CC)`, `$(LDSHARED)`, `$(CPP)`, `$(PKG_CONFIG)`)
|
|
9
|
+
# and runs them through `system(env, *command)`, so replacing just those keys
|
|
10
|
+
# with the rubycc executables makes `have_header`, `have_func`, `have_library`,
|
|
11
|
+
# `try_compile`/`try_link`/`try_run`, `check_sizeof` and the rest resolve
|
|
12
|
+
# against rubycc's own compiler and linker. mkmf.log stays untouched — mkmf
|
|
13
|
+
# writes it — so the log's format (the "checked program was:" sections, the
|
|
14
|
+
# echoed command lines) is the genuine article.
|
|
15
|
+
#
|
|
16
|
+
# Two RbConfig hashes are rewritten: CONFIG, which mkmf merges into every
|
|
17
|
+
# conftest command's expansion context, and MAKEFILE_CONFIG, which the generated
|
|
18
|
+
# Makefile's `CC = …`/`LDSHARED = …` assignments are drawn from — so both the
|
|
19
|
+
# probe phase and the eventual build route through rubycc. The change is
|
|
20
|
+
# in-process only (no file on disk is modified) and idempotent: requiring the
|
|
21
|
+
# shim twice is a no-op.
|
|
22
|
+
#
|
|
23
|
+
# The rubycc executables shebang-launch `require "rubycc"`, which needs the
|
|
24
|
+
# repository's `lib/` on the load path when rubycc is run from a checkout rather
|
|
25
|
+
# than an installed gem. The shim prepends that directory to `RUBYLIB` so the
|
|
26
|
+
# value propagates to the child processes mkmf spawns.
|
|
27
|
+
module Rubycc
|
|
28
|
+
module MkmfShim
|
|
29
|
+
# The repository's lib/ directory (this file lives at lib/rubycc/mkmf_shim.rb)
|
|
30
|
+
# and the gcc-compatible executables that stand in for the external toolchain.
|
|
31
|
+
LIB_DIR = File.expand_path("..", __dir__)
|
|
32
|
+
RUBYCC_EXE = File.expand_path("../../exe/rubycc", __dir__)
|
|
33
|
+
PKGCONF_EXE = File.expand_path("../../exe/rubycc-pkgconf", __dir__)
|
|
34
|
+
|
|
35
|
+
# The RbConfig keys rewritten to point at rubycc. Each maps to the command
|
|
36
|
+
# string mkmf embeds verbatim when it expands `$(CC)` and friends: a single
|
|
37
|
+
# shell-metacharacter-free executable path (plus, for the compound tools, the
|
|
38
|
+
# driver's own mode flag) so mkmf's `system(env, *command)` execs it directly.
|
|
39
|
+
def self.replacements
|
|
40
|
+
{
|
|
41
|
+
"CC" => RUBYCC_EXE,
|
|
42
|
+
"LDSHARED" => "#{RUBYCC_EXE} -shared",
|
|
43
|
+
"CPP" => "#{RUBYCC_EXE} -E",
|
|
44
|
+
"PKG_CONFIG" => PKGCONF_EXE
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Rewrites the toolchain keys in both RbConfig hashes and prepends the
|
|
49
|
+
# repository lib/ to RUBYLIB. Idempotent: the RUBYLIB entry is added only
|
|
50
|
+
# once, and re-assigning the same command strings is harmless.
|
|
51
|
+
def self.install!
|
|
52
|
+
configs = [RbConfig::CONFIG]
|
|
53
|
+
configs << RbConfig::MAKEFILE_CONFIG if defined?(RbConfig::MAKEFILE_CONFIG)
|
|
54
|
+
|
|
55
|
+
replacements.each do |key, value|
|
|
56
|
+
configs.each do |config|
|
|
57
|
+
# PKG_CONFIG is only meaningful if the host build already knew a
|
|
58
|
+
# pkg-config; leave an absent key absent so mkmf's own "no pkg-config"
|
|
59
|
+
# path is preserved, but override a present one (even the empty string
|
|
60
|
+
# RbConfig ships when none was found) to point at rubycc-pkgconf.
|
|
61
|
+
next if key == "PKG_CONFIG" && !config.key?(key)
|
|
62
|
+
|
|
63
|
+
config[key] = value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
prepend_rubylib!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Puts the repository lib/ at the front of RUBYLIB so a rubycc executable
|
|
71
|
+
# spawned by mkmf can `require "rubycc"` from a source checkout. Skips the
|
|
72
|
+
# work when the directory is already the leading entry.
|
|
73
|
+
def self.prepend_rubylib!
|
|
74
|
+
current = ENV["RUBYLIB"]
|
|
75
|
+
entries = current.to_s.split(File::PATH_SEPARATOR)
|
|
76
|
+
return if entries.first == LIB_DIR
|
|
77
|
+
|
|
78
|
+
ENV["RUBYLIB"] = [LIB_DIR, *current&.split(File::PATH_SEPARATOR)].join(File::PATH_SEPARATOR)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Rubycc::MkmfShim.install!
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubycc
|
|
4
|
+
module ObjFile
|
|
5
|
+
# Raised for any input that is not a well-formed GNU `ar` archive this reader
|
|
6
|
+
# accepts: a bad global magic, a member header that is truncated or missing
|
|
7
|
+
# its `\x60\n` terminator, a non-numeric size field, a size that runs past the
|
|
8
|
+
# end of the file, or a long-name reference that cannot be resolved against
|
|
9
|
+
# the `//` extended-name table. The message names the specific defect so the
|
|
10
|
+
# linker (L3), which reads static libraries through this class, can surface it.
|
|
11
|
+
class ArFormatError < Rubycc::Error; end
|
|
12
|
+
|
|
13
|
+
# The eight-byte magic that opens every `ar` archive.
|
|
14
|
+
AR_MAGIC = "!<arch>\n".b
|
|
15
|
+
|
|
16
|
+
# A single archive member: its resolved `name`, raw `data` bytes, and the four
|
|
17
|
+
# POSIX metadata fields (`mtime`/`uid`/`gid`/`mode`) the 60-byte header
|
|
18
|
+
# carries. `header_offset` is the byte position of that header in the whole
|
|
19
|
+
# image — the value the symbol index points at, so a symbol can be mapped back
|
|
20
|
+
# to the member that defines it. `special?` marks the two reserved members
|
|
21
|
+
# (`/` symbol table, `//` name table) that are archive metadata rather than
|
|
22
|
+
# real files, so callers listing or extracting members can skip them.
|
|
23
|
+
ArMember = Struct.new(
|
|
24
|
+
:name, :data, :mtime, :uid, :gid, :mode, :header_offset, :special,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
) do
|
|
27
|
+
def special? = special
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Reads a GNU-format `ar` archive into its members and (when present) its
|
|
31
|
+
# symbol index. BSD archives are out of scope; only `!<arch>\n` is accepted.
|
|
32
|
+
#
|
|
33
|
+
# The model mirrors ELFReader's load-then-query taste: `read`/`read_file`
|
|
34
|
+
# walk the whole image once into `#members` (in file order) and, if the
|
|
35
|
+
# archive carries one, a symbol name -> member map. That map is exactly what
|
|
36
|
+
# the linker's lazy-extraction driver queries ("which member defines this
|
|
37
|
+
# still-undefined symbol?"), so it is exposed both as the raw ordered
|
|
38
|
+
# `#symbols` list and the first-wins `#symbol_index` hash.
|
|
39
|
+
#
|
|
40
|
+
# Three reserved member names are understood: `/` is the classic ranlib
|
|
41
|
+
# symbol table (big-endian uint32 count, that many big-endian uint32
|
|
42
|
+
# member-header offsets, then NUL-terminated symbol names); `//` is the
|
|
43
|
+
# extended-name table holding long member names each terminated by `/\n`; and
|
|
44
|
+
# `/N` in a member's name field is a decimal byte offset into that `//` table.
|
|
45
|
+
# A short name is stored as `name/`, the trailing slash marking its end so a
|
|
46
|
+
# name may keep trailing spaces. Archives written without a symbol index
|
|
47
|
+
# (plain `ar qc`) are tolerated: `#symbols` is then empty.
|
|
48
|
+
class ArReader
|
|
49
|
+
HEADER_SIZE = 60
|
|
50
|
+
|
|
51
|
+
# Field offsets and widths within a 60-byte member header. The five ASCII
|
|
52
|
+
# fields are space-padded; only `size` (decimal) is needed to walk the
|
|
53
|
+
# archive, the rest are decoded for the member's metadata.
|
|
54
|
+
NAME_OFF = 0
|
|
55
|
+
NAME_LEN = 16
|
|
56
|
+
MTIME_OFF = 16
|
|
57
|
+
MTIME_LEN = 12
|
|
58
|
+
UID_OFF = 28
|
|
59
|
+
UID_LEN = 6
|
|
60
|
+
GID_OFF = 34
|
|
61
|
+
GID_LEN = 6
|
|
62
|
+
MODE_OFF = 40
|
|
63
|
+
MODE_LEN = 8
|
|
64
|
+
SIZE_OFF = 48
|
|
65
|
+
SIZE_LEN = 10
|
|
66
|
+
MAGIC_OFF = 58
|
|
67
|
+
HEADER_MAGIC = "\x60\x0a".b
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
# Parses an in-memory archive (an ASCII-8BIT String) into a ready-to-query
|
|
71
|
+
# reader.
|
|
72
|
+
def read(bytes)
|
|
73
|
+
new(bytes).tap(&:parse!)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Reads an archive file from disk and parses it.
|
|
77
|
+
def read_file(path)
|
|
78
|
+
read(File.binread(path))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
attr_reader :members, :symbols, :name_table
|
|
83
|
+
|
|
84
|
+
def initialize(bytes)
|
|
85
|
+
@data = bytes.b
|
|
86
|
+
@members = []
|
|
87
|
+
@symbols = []
|
|
88
|
+
@name_table = nil
|
|
89
|
+
@symtab_raw = nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse!
|
|
93
|
+
unless @data.byteslice(0, AR_MAGIC.bytesize) == AR_MAGIC
|
|
94
|
+
raise ArFormatError, "not an ar archive (bad global magic)"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
pos = AR_MAGIC.bytesize
|
|
98
|
+
# This loop always terminates: parse_member advances pos by at least the
|
|
99
|
+
# 60-byte header plus a non-negative member size (a size field that is not
|
|
100
|
+
# a non-negative decimal is rejected outright), so pos strictly increases
|
|
101
|
+
# by >= 60 each turn and reaches @data.bytesize in a bounded number of
|
|
102
|
+
# steps — a malformed archive cannot make it stall or loop.
|
|
103
|
+
pos = parse_member(pos) while pos < @data.bytesize
|
|
104
|
+
resolve_symbol_index
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The member with the given name, or nil. Regular members only — the `/`
|
|
109
|
+
# and `//` metadata members never carry a user-facing name.
|
|
110
|
+
def member(name)
|
|
111
|
+
@members.find { |m| !m.special? && m.name == name }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# symbol name -> defining member, first definition winning when a symbol is
|
|
115
|
+
# exported by more than one member (the order the linker resolves in).
|
|
116
|
+
def symbol_index
|
|
117
|
+
@symbol_index ||= @symbols.each_with_object({}) do |entry, map|
|
|
118
|
+
map[entry[:name]] ||= entry[:member]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The member that defines `symbol_name`, or nil — the query the lazy linker
|
|
123
|
+
# asks for each still-undefined reference.
|
|
124
|
+
def member_defining(symbol_name)
|
|
125
|
+
symbol_index[symbol_name]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# Parses one member starting at `pos` and returns the offset of the next
|
|
131
|
+
# member (past the even-boundary padding byte). The `/` and `//` reserved
|
|
132
|
+
# members are recorded like any other but flagged special and, for `//`,
|
|
133
|
+
# captured as the name table so later `/N` references resolve.
|
|
134
|
+
def parse_member(pos)
|
|
135
|
+
if pos + HEADER_SIZE > @data.bytesize
|
|
136
|
+
raise ArFormatError, "truncated member header at offset #{pos}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
header = @data.byteslice(pos, HEADER_SIZE)
|
|
140
|
+
unless header.byteslice(MAGIC_OFF, 2) == HEADER_MAGIC
|
|
141
|
+
raise ArFormatError, "member header at offset #{pos} lacks its `\\x60\\n` terminator"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
size = decimal_field(header, SIZE_OFF, SIZE_LEN, "size", pos)
|
|
145
|
+
data_off = pos + HEADER_SIZE
|
|
146
|
+
if data_off + size > @data.bytesize
|
|
147
|
+
raise ArFormatError, "member at offset #{pos} declares #{size} bytes running past end of file"
|
|
148
|
+
end
|
|
149
|
+
data = @data.byteslice(data_off, size)
|
|
150
|
+
|
|
151
|
+
raw_name = header.byteslice(NAME_OFF, NAME_LEN)
|
|
152
|
+
kind, resolved = classify_name(raw_name, pos)
|
|
153
|
+
|
|
154
|
+
case kind
|
|
155
|
+
when :symtab
|
|
156
|
+
@symtab_raw = data
|
|
157
|
+
when :nametable
|
|
158
|
+
@name_table = data
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
@members << ArMember.new(
|
|
162
|
+
name: resolved,
|
|
163
|
+
data: data,
|
|
164
|
+
mtime: decimal_field(header, MTIME_OFF, MTIME_LEN, "mtime", pos, allow_blank: true),
|
|
165
|
+
uid: decimal_field(header, UID_OFF, UID_LEN, "uid", pos, allow_blank: true),
|
|
166
|
+
gid: decimal_field(header, GID_OFF, GID_LEN, "gid", pos, allow_blank: true),
|
|
167
|
+
mode: octal_field(header, MODE_OFF, MODE_LEN, pos),
|
|
168
|
+
header_offset: pos,
|
|
169
|
+
special: kind == :symtab || kind == :nametable
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Member data is padded with a single `\n` to an even offset; the size
|
|
173
|
+
# field counts only the real bytes, so an odd size means one pad byte.
|
|
174
|
+
data_off + size + (size.odd? ? 1 : 0)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Decodes the 16-byte name field into [kind, name]. `/` alone is the symbol
|
|
178
|
+
# table, `//` the extended-name table, `/N` a long-name reference resolved
|
|
179
|
+
# through that table, and anything else a short `name/` (or, defensively, a
|
|
180
|
+
# bare name) with its trailing slash stripped.
|
|
181
|
+
def classify_name(raw_name, pos)
|
|
182
|
+
trimmed = raw_name.rstrip
|
|
183
|
+
if trimmed == "/"
|
|
184
|
+
[:symtab, "/"]
|
|
185
|
+
elsif trimmed == "//"
|
|
186
|
+
[:nametable, "//"]
|
|
187
|
+
elsif trimmed.start_with?("/") && trimmed.byteslice(1..).match?(/\A\d+\z/)
|
|
188
|
+
[:long, resolve_long_name(trimmed.byteslice(1..).to_i, pos)]
|
|
189
|
+
else
|
|
190
|
+
[:short, trimmed.chomp("/")]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Resolves a `/N` reference: read from byte `offset` in the `//` table up to
|
|
195
|
+
# the terminating `\n` and drop the trailing `/` GNU appends to each entry.
|
|
196
|
+
def resolve_long_name(offset, pos)
|
|
197
|
+
table = @name_table or
|
|
198
|
+
raise ArFormatError, "member at offset #{pos} references a name table that is absent"
|
|
199
|
+
if offset >= table.bytesize
|
|
200
|
+
raise ArFormatError, "member at offset #{pos} references name-table offset #{offset} out of range"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
stop = table.index("\n".b, offset) || table.bytesize
|
|
204
|
+
table.byteslice(offset...stop).chomp("/").force_encoding(Encoding::UTF_8)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Turns the raw `/` symbol table into name -> member entries. Deferred until
|
|
208
|
+
# every member's header_offset is known so each big-endian offset can be
|
|
209
|
+
# matched to the member it names. A symbol pointing at an unknown offset is
|
|
210
|
+
# skipped rather than fatal — a stale index should not sink an otherwise
|
|
211
|
+
# readable archive.
|
|
212
|
+
def resolve_symbol_index
|
|
213
|
+
return if @symtab_raw.nil? || @symtab_raw.bytesize < 4
|
|
214
|
+
|
|
215
|
+
by_offset = @members.each_with_object({}) { |m, h| h[m.header_offset] = m }
|
|
216
|
+
count = @symtab_raw.byteslice(0, 4).unpack1("N")
|
|
217
|
+
offsets_end = 4 + count * 4
|
|
218
|
+
return if offsets_end > @symtab_raw.bytesize
|
|
219
|
+
|
|
220
|
+
offsets = @symtab_raw.byteslice(4, count * 4).unpack("N*")
|
|
221
|
+
names_blob = @symtab_raw.byteslice(offsets_end..) || "".b
|
|
222
|
+
|
|
223
|
+
cursor = 0
|
|
224
|
+
count.times do |i|
|
|
225
|
+
stop = names_blob.index("\0".b, cursor) or break
|
|
226
|
+
name = names_blob.byteslice(cursor...stop).force_encoding(Encoding::UTF_8)
|
|
227
|
+
cursor = stop + 1
|
|
228
|
+
member = by_offset[offsets[i]]
|
|
229
|
+
@symbols << { name: name, member: member } if member
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Reads a space-padded ASCII decimal field. A field of all spaces decodes to
|
|
234
|
+
# 0 when `allow_blank` (GNU leaves the deterministic uid/gid/mtime blank in
|
|
235
|
+
# some writers); a non-numeric size or similar is a format error.
|
|
236
|
+
def decimal_field(header, offset, length, what, pos, allow_blank: false)
|
|
237
|
+
text = header.byteslice(offset, length).strip
|
|
238
|
+
return 0 if text.empty? && allow_blank
|
|
239
|
+
unless text.match?(/\A\d+\z/)
|
|
240
|
+
raise ArFormatError, "member at offset #{pos} has a non-numeric #{what} field #{text.inspect}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
text.to_i
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Reads the octal mode field; a blank field means 0.
|
|
247
|
+
def octal_field(header, offset, length, pos)
|
|
248
|
+
text = header.byteslice(offset, length).strip
|
|
249
|
+
return 0 if text.empty?
|
|
250
|
+
unless text.match?(/\A[0-7]+\z/)
|
|
251
|
+
raise ArFormatError, "member at offset #{pos} has a non-octal mode field #{text.inspect}"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
text.to_i(8)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Builds a GNU-format `ar` archive. The counterpart of ArReader, mirroring
|
|
259
|
+
# ELFWriter's builder taste: members are appended in order with #add_member
|
|
260
|
+
# and the whole image is assembled by #to_binary.
|
|
261
|
+
#
|
|
262
|
+
# Every archive is written with a classic ranlib symbol index as its first
|
|
263
|
+
# `/` member (always, even when empty), so output is a drop-in for `ar rcs`
|
|
264
|
+
# and the linker can extract lazily. The index's symbols are gathered by
|
|
265
|
+
# handing each member's bytes to ELFReader and taking its defined global and
|
|
266
|
+
# weak symbols; a member ELFReader cannot parse (a non-ELF file) simply
|
|
267
|
+
# contributes none. Long member names (over 15 characters, once the trailing
|
|
268
|
+
# `/` is counted) move to a `//` extended-name table and are referenced by a
|
|
269
|
+
# `/N` byte offset; shorter names are stored inline as `name/`.
|
|
270
|
+
#
|
|
271
|
+
# Output is fully deterministic (N4): mtime, uid and gid are pinned to 0 and
|
|
272
|
+
# the mode to 0644 (the metadata GNU `ar` writes in deterministic mode), so
|
|
273
|
+
# identical member bytes always produce a byte-identical archive.
|
|
274
|
+
class ArWriter
|
|
275
|
+
# Pinned deterministic member metadata. The reserved `/` and `//` members
|
|
276
|
+
# carry a 0 mode, matching GNU.
|
|
277
|
+
DETERMINISTIC_MTIME = "0"
|
|
278
|
+
DETERMINISTIC_UID = "0"
|
|
279
|
+
DETERMINISTIC_GID = "0"
|
|
280
|
+
REGULAR_MODE = "644"
|
|
281
|
+
SPECIAL_MODE = "0"
|
|
282
|
+
|
|
283
|
+
# A name plus its trailing-slash form fits inline when at most this many
|
|
284
|
+
# bytes; a longer name goes to the `//` table.
|
|
285
|
+
INLINE_NAME_CAPACITY = 16
|
|
286
|
+
|
|
287
|
+
def initialize
|
|
288
|
+
@members = []
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Appends a member with the given name and raw data. Names are not
|
|
292
|
+
# de-duplicated here — replace-or-append is the CLI's job; the writer lays
|
|
293
|
+
# out exactly what it is given, in order.
|
|
294
|
+
def add_member(name, data)
|
|
295
|
+
@members << { name: name.to_s, data: data.b }
|
|
296
|
+
self
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Assembles and returns the archive as an ASCII-8BIT String.
|
|
300
|
+
def to_binary
|
|
301
|
+
entries = @members.map { |m| { name: m[:name], data: m[:data], symbols: gather_symbols(m[:data]) } }
|
|
302
|
+
|
|
303
|
+
name_table, name_field = build_name_table(entries)
|
|
304
|
+
flat_symbols = flatten_symbols(entries)
|
|
305
|
+
symtab_size = symtab_byte_size(flat_symbols)
|
|
306
|
+
|
|
307
|
+
# The symbol index and the name table precede every regular member, so
|
|
308
|
+
# each regular member's header offset — the value the index records — is
|
|
309
|
+
# known once their spans are summed. The index's own size depends only on
|
|
310
|
+
# the symbol count and names, not on the offsets it will hold, so there is
|
|
311
|
+
# no circular dependency.
|
|
312
|
+
base = AR_MAGIC.bytesize + member_span(symtab_size)
|
|
313
|
+
base += member_span(name_table.bytesize) unless name_table.empty?
|
|
314
|
+
offset = base
|
|
315
|
+
entries.each do |entry|
|
|
316
|
+
entry[:header_offset] = offset
|
|
317
|
+
offset += member_span(entry[:data].bytesize)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
symtab = build_symtab(flat_symbols, entries)
|
|
321
|
+
|
|
322
|
+
out = +"".b
|
|
323
|
+
out << AR_MAGIC
|
|
324
|
+
emit_member(out, "/", symtab, SPECIAL_MODE)
|
|
325
|
+
emit_member(out, "//", name_table, SPECIAL_MODE) unless name_table.empty?
|
|
326
|
+
entries.each { |entry| emit_member(out, name_field[entry.object_id], entry[:data], REGULAR_MODE) }
|
|
327
|
+
out
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Convenience: assemble and write the archive to `path`.
|
|
331
|
+
def write(path)
|
|
332
|
+
File.binwrite(path, to_binary)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
private
|
|
336
|
+
|
|
337
|
+
# The defined global and weak symbol names an ELF member exports, gathered
|
|
338
|
+
# through ELFReader so no ELF is re-parsed by hand. A non-ELF member (or one
|
|
339
|
+
# ELFReader rejects) contributes nothing.
|
|
340
|
+
def gather_symbols(data)
|
|
341
|
+
reader = ELFReader.read(data)
|
|
342
|
+
reader.symbols.select do |sym|
|
|
343
|
+
(sym.bind == :global || sym.bind == :weak) && !sym.undefined? && !sym.name.to_s.empty?
|
|
344
|
+
end.map(&:name)
|
|
345
|
+
rescue ELFFormatError
|
|
346
|
+
[]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Assigns every member a name field, moving long names into the `//` table.
|
|
350
|
+
# Returns [table_bytes, entry.object_id -> name field]. A name fits inline
|
|
351
|
+
# when `name/` is at most 16 bytes; otherwise it is appended to the table
|
|
352
|
+
# (terminated by `/\n`, GNU's convention) and referenced by `/offset`.
|
|
353
|
+
def build_name_table(entries)
|
|
354
|
+
table = +"".b
|
|
355
|
+
fields = {}
|
|
356
|
+
entries.each do |entry|
|
|
357
|
+
name = entry[:name]
|
|
358
|
+
if name.bytesize + 1 > INLINE_NAME_CAPACITY
|
|
359
|
+
fields[entry.object_id] = "/#{table.bytesize}"
|
|
360
|
+
table << name.b << "/\n".b
|
|
361
|
+
else
|
|
362
|
+
fields[entry.object_id] = "#{name}/"
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
[table, fields]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Flattens every member's symbols into [name, entry] pairs in member order,
|
|
369
|
+
# so the index lists a member's exports contiguously and points them all at
|
|
370
|
+
# that member's header.
|
|
371
|
+
def flatten_symbols(entries)
|
|
372
|
+
entries.flat_map { |entry| entry[:symbols].map { |name| [name, entry] } }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# The `/` symbol table's byte size: the uint32 count, one uint32 per symbol
|
|
376
|
+
# offset, then each NUL-terminated name.
|
|
377
|
+
def symtab_byte_size(flat_symbols)
|
|
378
|
+
4 + 4 * flat_symbols.size + flat_symbols.sum { |(name, _)| name.bytesize + 1 }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Builds the `/` symbol table body once member header offsets are fixed:
|
|
382
|
+
# big-endian count, big-endian member-header offsets, then the names.
|
|
383
|
+
def build_symtab(flat_symbols, _entries)
|
|
384
|
+
buf = +"".b
|
|
385
|
+
buf << [flat_symbols.size].pack("N")
|
|
386
|
+
flat_symbols.each { |(_, entry)| buf << [entry[:header_offset]].pack("N") }
|
|
387
|
+
flat_symbols.each { |(name, _)| buf << name.b << "\0".b }
|
|
388
|
+
buf
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The whole-file span a member occupies: its 60-byte header, its data, and
|
|
392
|
+
# the single `\n` pad that keeps the next member on an even offset.
|
|
393
|
+
def member_span(data_size)
|
|
394
|
+
HEADER_SIZE + data_size + (data_size.odd? ? 1 : 0)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
HEADER_SIZE = 60
|
|
398
|
+
|
|
399
|
+
# Appends a member (header, data, and the even-boundary pad byte) to `out`.
|
|
400
|
+
def emit_member(out, name_field, data, mode)
|
|
401
|
+
out << member_header(name_field, data.bytesize, mode)
|
|
402
|
+
out << data
|
|
403
|
+
out << "\n".b if data.bytesize.odd?
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# A 60-byte member header. Every ASCII field is left-justified and
|
|
407
|
+
# space-padded; the deterministic metadata is pinned and only name, size and
|
|
408
|
+
# mode vary between members.
|
|
409
|
+
def member_header(name_field, size, mode)
|
|
410
|
+
header = +"".b
|
|
411
|
+
header << pad(name_field, NAME_LEN)
|
|
412
|
+
header << pad(DETERMINISTIC_MTIME, MTIME_LEN)
|
|
413
|
+
header << pad(DETERMINISTIC_UID, UID_LEN)
|
|
414
|
+
header << pad(DETERMINISTIC_GID, GID_LEN)
|
|
415
|
+
header << pad(mode, MODE_LEN)
|
|
416
|
+
header << pad(size.to_s, SIZE_LEN)
|
|
417
|
+
header << HEADER_MAGIC
|
|
418
|
+
header
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
NAME_LEN = 16
|
|
422
|
+
MTIME_LEN = 12
|
|
423
|
+
UID_LEN = 6
|
|
424
|
+
GID_LEN = 6
|
|
425
|
+
MODE_LEN = 8
|
|
426
|
+
SIZE_LEN = 10
|
|
427
|
+
HEADER_MAGIC = "\x60\x0a".b
|
|
428
|
+
|
|
429
|
+
def pad(text, width)
|
|
430
|
+
bytes = text.to_s.b
|
|
431
|
+
raise ArFormatError, "field #{text.inspect} exceeds #{width} bytes" if bytes.bytesize > width
|
|
432
|
+
|
|
433
|
+
bytes + (" ".b * (width - bytes.bytesize))
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|