|
| 1 | +import 'dart:io'; |
| 2 | +import 'dart:typed_data'; |
| 3 | + |
| 4 | +const _libraryName = 'libbdk_dart_ffi.so'; |
| 5 | +const _minimumLoadAlignment = 0x4000; |
| 6 | +const _ptLoad = 1; |
| 7 | + |
| 8 | +Future<void> main(List<String> args) async { |
| 9 | + if (args.isEmpty) { |
| 10 | + _fail( |
| 11 | + 'Usage: dart scripts/check_android_elf_alignment.dart ' |
| 12 | + '<apk-aab-or-so> [...]', |
| 13 | + ); |
| 14 | + } |
| 15 | + |
| 16 | + for (final path in args) { |
| 17 | + final input = File(path); |
| 18 | + if (!input.existsSync()) { |
| 19 | + _fail('File not found: ${input.path}'); |
| 20 | + } |
| 21 | + |
| 22 | + if (_isElf(input)) { |
| 23 | + _checkLibrary(input, input.path); |
| 24 | + } else { |
| 25 | + await _checkArchive(input); |
| 26 | + } |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +Future<void> _checkArchive(File archive) async { |
| 31 | + final tempDir = await Directory.systemTemp.createTemp( |
| 32 | + 'bdk_dart_elf_alignment_', |
| 33 | + ); |
| 34 | + |
| 35 | + try { |
| 36 | + final entries = await _findNativeLibraryEntries(archive); |
| 37 | + if (entries.isEmpty) { |
| 38 | + _fail('No $_libraryName entries found in ${archive.path}'); |
| 39 | + } |
| 40 | + |
| 41 | + await _extractLibraries(archive, tempDir, entries); |
| 42 | + |
| 43 | + for (final entry in entries) { |
| 44 | + final library = _extractedEntry(tempDir, entry); |
| 45 | + if (!library.existsSync()) { |
| 46 | + _fail('Extracted library not found: $entry'); |
| 47 | + } |
| 48 | + _checkLibrary(library, entry); |
| 49 | + } |
| 50 | + } finally { |
| 51 | + await tempDir.delete(recursive: true); |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +void _checkLibrary(File library, String label) { |
| 56 | + final loadAlignments = _readLoadAlignments(library); |
| 57 | + if (loadAlignments.isEmpty) { |
| 58 | + _fail('No PT_LOAD segments found in $label'); |
| 59 | + } |
| 60 | + |
| 61 | + final invalidAlignments = loadAlignments.where( |
| 62 | + (alignment) => |
| 63 | + alignment < _minimumLoadAlignment || |
| 64 | + alignment % _minimumLoadAlignment != 0, |
| 65 | + ); |
| 66 | + |
| 67 | + if (invalidAlignments.isNotEmpty) { |
| 68 | + _fail( |
| 69 | + '$label has invalid PT_LOAD alignment(s): ' |
| 70 | + '${invalidAlignments.map(_hex).join(', ')}', |
| 71 | + ); |
| 72 | + } |
| 73 | + |
| 74 | + final minimumAlignment = loadAlignments.reduce((a, b) => a < b ? a : b); |
| 75 | + stdout.writeln('OK $label minLOADalign=${_hex(minimumAlignment)}'); |
| 76 | +} |
| 77 | + |
| 78 | +Future<List<String>> _findNativeLibraryEntries(File archive) async { |
| 79 | + final result = await Process.run('unzip', ['-Z', '-1', archive.path]); |
| 80 | + if (result.exitCode != 0) { |
| 81 | + _fail( |
| 82 | + 'Failed to list $_libraryName entries in ${archive.path}.\n' |
| 83 | + '${result.stderr}', |
| 84 | + ); |
| 85 | + } |
| 86 | + |
| 87 | + final entries = |
| 88 | + (result.stdout as String) |
| 89 | + .split('\n') |
| 90 | + .where(_isTargetLibraryEntry) |
| 91 | + .toList() |
| 92 | + ..sort(); |
| 93 | + return entries; |
| 94 | +} |
| 95 | + |
| 96 | +bool _isTargetLibraryEntry(String entry) { |
| 97 | + final segments = entry.split('/'); |
| 98 | + if (segments.any((segment) => segment.isEmpty || segment == '.')) { |
| 99 | + return false; |
| 100 | + } |
| 101 | + if (segments.any((segment) => segment == '..')) { |
| 102 | + return false; |
| 103 | + } |
| 104 | + if (segments.last != _libraryName) { |
| 105 | + return false; |
| 106 | + } |
| 107 | + |
| 108 | + final isApkLibrary = segments.length == 3 && segments[0] == 'lib'; |
| 109 | + final isAabLibrary = segments.length == 4 && segments[1] == 'lib'; |
| 110 | + return isApkLibrary || isAabLibrary; |
| 111 | +} |
| 112 | + |
| 113 | +Future<void> _extractLibraries( |
| 114 | + File archive, |
| 115 | + Directory destination, |
| 116 | + List<String> entries, |
| 117 | +) async { |
| 118 | + final result = await Process.run('unzip', [ |
| 119 | + '-q', |
| 120 | + archive.path, |
| 121 | + ...entries, |
| 122 | + '-d', |
| 123 | + destination.path, |
| 124 | + ]); |
| 125 | + |
| 126 | + if (result.exitCode != 0) { |
| 127 | + _fail( |
| 128 | + 'Failed to extract $_libraryName from ${archive.path}.\n' |
| 129 | + '${result.stderr}', |
| 130 | + ); |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +File _extractedEntry(Directory root, String entry) { |
| 135 | + final path = entry |
| 136 | + .split('/') |
| 137 | + .fold(root.path, (parent, child) => _join(parent, child)); |
| 138 | + return File(path); |
| 139 | +} |
| 140 | + |
| 141 | +bool _isElf(File file) { |
| 142 | + final reader = file.openSync(); |
| 143 | + try { |
| 144 | + final magic = reader.readSync(4); |
| 145 | + return magic.length == 4 && |
| 146 | + magic[0] == 0x7f && |
| 147 | + magic[1] == 0x45 && |
| 148 | + magic[2] == 0x4c && |
| 149 | + magic[3] == 0x46; |
| 150 | + } finally { |
| 151 | + reader.closeSync(); |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +List<int> _readLoadAlignments(File elfFile) { |
| 156 | + final bytes = elfFile.readAsBytesSync(); |
| 157 | + if (bytes.length < 64 || !_isElf(elfFile)) { |
| 158 | + _fail('${elfFile.path} is not an ELF file'); |
| 159 | + } |
| 160 | + |
| 161 | + final elfClass = bytes[4]; |
| 162 | + final endian = switch (bytes[5]) { |
| 163 | + 1 => Endian.little, |
| 164 | + 2 => Endian.big, |
| 165 | + _ => throw FormatException('Unsupported ELF endianness in ${elfFile.path}'), |
| 166 | + }; |
| 167 | + final data = ByteData.sublistView(bytes); |
| 168 | + |
| 169 | + int uint16(int offset) => data.getUint16(offset, endian); |
| 170 | + int uint32(int offset) => data.getUint32(offset, endian); |
| 171 | + int uint64(int offset) => data.getUint64(offset, endian); |
| 172 | + |
| 173 | + final ( |
| 174 | + programHeaderOffset, |
| 175 | + programHeaderEntrySize, |
| 176 | + programHeaderCount, |
| 177 | + ) = switch (elfClass) { |
| 178 | + 1 => (uint32(28), uint16(42), uint16(44)), |
| 179 | + 2 => (uint64(32), uint16(54), uint16(56)), |
| 180 | + _ => throw FormatException('Unsupported ELF class in ${elfFile.path}'), |
| 181 | + }; |
| 182 | + |
| 183 | + final alignments = <int>[]; |
| 184 | + for (var index = 0; index < programHeaderCount; index++) { |
| 185 | + final headerOffset = programHeaderOffset + index * programHeaderEntrySize; |
| 186 | + if (headerOffset + programHeaderEntrySize > bytes.length) { |
| 187 | + _fail('${elfFile.path} has a truncated ELF program header table'); |
| 188 | + } |
| 189 | + |
| 190 | + final type = uint32(headerOffset); |
| 191 | + if (type != _ptLoad) { |
| 192 | + continue; |
| 193 | + } |
| 194 | + |
| 195 | + final alignment = switch (elfClass) { |
| 196 | + 1 => uint32(headerOffset + 28), |
| 197 | + 2 => uint64(headerOffset + 48), |
| 198 | + _ => throw StateError('unreachable'), |
| 199 | + }; |
| 200 | + alignments.add(alignment); |
| 201 | + } |
| 202 | + |
| 203 | + return alignments; |
| 204 | +} |
| 205 | + |
| 206 | +String _hex(int value) => '0x${value.toRadixString(16)}'; |
| 207 | + |
| 208 | +String _join(String parent, String child) { |
| 209 | + final separator = Platform.pathSeparator; |
| 210 | + return parent.endsWith(separator) |
| 211 | + ? '$parent$child' |
| 212 | + : '$parent$separator$child'; |
| 213 | +} |
| 214 | + |
| 215 | +Never _fail(String message) { |
| 216 | + stderr.writeln(message); |
| 217 | + exit(1); |
| 218 | +} |
0 commit comments