// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as p;

// This script verifies that the release binaries only export the expected
// symbols.
//
// Android binaries (libflutter.so) should only export one symbol "JNI_OnLoad"
// of type "T".
//
// iOS binaries (Flutter.framework/Flutter) should only export Objective-C
// Symbols from the Flutter namespace. These are either of type
// "(__DATA,__common)" or "(__DATA,__objc_data)".

/// Takes the path to the out directory as the first argument, and the path to
/// the buildtools directory as the second argument.
///
/// If the second argument is not specified, for backwards compatibility, it is
/// assumed that it is ../buildtools relative to the first parameter (the out
/// directory).
void main(List<String> arguments) {
  if (arguments.isEmpty || arguments.length > 2) {
    print('usage: dart verify_exported.dart OUT_DIR [BUILDTOOLS]');
    exit(1);
  }
  String outPath = arguments.first;
  if (p.isRelative(outPath)) {
    /// If path is relative then create a full path starting from the engine checkout
    /// repository.
    if (!Platform.environment.containsKey('ENGINE_CHECKOUT_PATH')) {
      print('ENGINE_CHECKOUT_PATH env variable is mandatory when using relative destination path');
      exit(1);
    }
    final String engineCheckoutPath = Platform.environment['ENGINE_CHECKOUT_PATH']!;
    outPath = p.join(engineCheckoutPath, outPath);
  }
  final String buildToolsPath = arguments.length == 1
      ? p.join(p.dirname(outPath), 'buildtools')
      : arguments[1];

  String platform;
  if (Platform.isLinux) {
    platform = 'linux-x64';
  } else if (Platform.isMacOS) {
    platform = 'mac-x64';
  } else {
    throw UnimplementedError('Script only support running on Linux or MacOS.');
  }
  final String nmPath = p.join(buildToolsPath, platform, 'clang', 'bin', 'llvm-nm');
  if (!Directory(outPath).existsSync()) {
    print('error: build out directory not found: $outPath');
    exit(1);
  }

  final Iterable<String> releaseBuilds = Directory(outPath).listSync()
      .whereType<Directory>()
      .map<String>((FileSystemEntity dir) => p.basename(dir.path))
      .where((String s) => s.contains('_release'));

  final Iterable<String> iosReleaseBuilds = releaseBuilds
      .where((String s) => s.startsWith('ios_'));
  final Iterable<String> androidReleaseBuilds = releaseBuilds
      .where((String s) => s.startsWith('android_'));
  final Iterable<String> hostReleaseBuilds = releaseBuilds
      .where((String s) => s.startsWith('host_'));

  int failures = 0;
  failures += _checkIos(outPath, nmPath, iosReleaseBuilds);
  failures += _checkAndroid(outPath, nmPath, androidReleaseBuilds);
  if (Platform.isLinux) {
    failures += _checkLinux(outPath, nmPath, hostReleaseBuilds);
  }
  print('Failing checks: $failures');
  exit(failures);
}

int _checkIos(String outPath, String nmPath, Iterable<String> builds) {
  int failures = 0;
  for (final String build in builds) {
    final String libFlutter = p.join(outPath, build, 'Flutter.framework', 'Flutter');
    if (!File(libFlutter).existsSync()) {
      print('SKIPPING: $libFlutter does not exist.');
      continue;
    }
    final ProcessResult nmResult = Process.runSync(nmPath, <String>['-gUm', libFlutter]);
    if (nmResult.exitCode != 0) {
      print('ERROR: failed to execute "nm -gUm $libFlutter":\n${nmResult.stderr}');
      failures++;
      continue;
    }
    final Iterable<NmEntry> unexpectedEntries = NmEntry.parse(nmResult.stdout as String).where((NmEntry entry) {
      final bool cSymbol = (entry.type == '(__DATA,__common)' || entry.type == '(__DATA,__const)')
          && entry.name.startsWith('_Flutter');
      final bool cInternalSymbol = entry.type == '(__TEXT,__text)' && entry.name.startsWith('_InternalFlutter');
      final bool objcSymbol = entry.type == '(__DATA,__objc_data)'
          && (entry.name.startsWith(r'_OBJC_METACLASS_$_Flutter') || entry.name.startsWith(r'_OBJC_CLASS_$_Flutter'));
      return !(cSymbol || cInternalSymbol || objcSymbol);
    });
    if (unexpectedEntries.isNotEmpty) {
      print('ERROR: $libFlutter exports unexpected symbols:');
      print(unexpectedEntries.fold<String>('', (String previous, NmEntry entry) {
        return '${previous == '' ? '' : '$previous\n'}     ${entry.type} ${entry.name}';
      }));
      failures++;
    } else {
      print('OK: $libFlutter');
    }
  }
  return failures;
}

int _checkAndroid(String outPath, String nmPath, Iterable<String> builds) {
  int failures = 0;
  for (final String build in builds) {
    final String libFlutter = p.join(outPath, build, 'libflutter.so');
    if (!File(libFlutter).existsSync()) {
      print('SKIPPING: $libFlutter does not exist.');
      continue;
    }
    final ProcessResult nmResult = Process.runSync(nmPath, <String>['-gU', libFlutter]);
    if (nmResult.exitCode != 0) {
      print('ERROR: failed to execute "nm -gU $libFlutter":\n${nmResult.stderr}');
      failures++;
      continue;
    }
    final Iterable<NmEntry> entries = NmEntry.parse(nmResult.stdout as String);
    final Map<String, String> entryMap = <String, String>{
      for (final NmEntry entry in entries)
        entry.name: entry.type,
    };
    final Map<String, String> expectedSymbols = <String, String>{
      'JNI_OnLoad': 'T',
      '_binary_icudtl_dat_size': 'A',
      '_binary_icudtl_dat_start': 'D',
      // TODO(dnfield): Remove these once Clang lld does not expose them.
      // arm https://bugs.fuchsia.dev/p/fuchsia/issues/detail?id=47943
      '__adddf3': 'T',
      '__addsf3': 'T',
      '__aeabi_cdcmpeq': 'T',
      '__aeabi_cdcmple': 'T',
      '__aeabi_cdrcmple': 'T',
      '__aeabi_cfcmpeq': 'T',
      '__aeabi_cfcmple': 'T',
      '__aeabi_cfrcmple': 'T',
      '__aeabi_d2lz': 'T',
      '__aeabi_d2uiz': 'T',
      '__aeabi_d2ulz': 'T',
      '__aeabi_dadd': 'T',
      '__aeabi_dcmpeq': 'T',
      '__aeabi_dcmpge': 'T',
      '__aeabi_dcmpgt': 'T',
      '__aeabi_dcmple': 'T',
      '__aeabi_dcmplt': 'T',
      '__aeabi_ddiv': 'T',
      '__aeabi_dmul': 'T',
      '__aeabi_drsub': 'T',
      '__aeabi_dsub': 'T',
      '__aeabi_f2d': 'T',
      '__aeabi_f2lz': 'T',
      '__aeabi_f2ulz': 'T',
      '__aeabi_fadd': 'T',
      '__aeabi_fcmpeq': 'T',
      '__aeabi_fcmpge': 'T',
      '__aeabi_fcmpgt': 'T',
      '__aeabi_fcmple': 'T',
      '__aeabi_fcmplt': 'T',
      '__aeabi_frsub': 'T',
      '__aeabi_fsub': 'T',
      '__aeabi_i2d': 'T',
      '__aeabi_i2f': 'T',
      '__aeabi_l2d': 'T',
      '__aeabi_l2f': 'T',
      '__aeabi_lasr': 'T',
      '__aeabi_ldivmod': 'T',
      '__aeabi_llsl': 'T',
      '__aeabi_llsr': 'T',
      '__aeabi_ui2d': 'T',
      '__aeabi_ui2f': 'T',
      '__aeabi_uidiv': 'T',
      '__aeabi_uidivmod': 'T',
      '__aeabi_ul2d': 'T',
      '__aeabi_ul2f': 'T',
      '__aeabi_uldivmod': 'T',
      '__ashldi3': 'T',
      '__ashrdi3': 'T',
      '__cmpdf2': 'T',
      '__cmpsf2': 'T',
      '__divdf3': 'T',
      '__divdi3': 'T',
      '__eqdf2': 'T',
      '__eqsf2': 'T',
      '__extendsfdf2': 'T',
      '__fixdfdi': 'T',
      '__fixsfdi': 'T',
      '__fixunsdfdi': 'T',
      '__fixunsdfsi': 'T',
      '__fixunssfdi': 'T',
      '__floatdidf': 'T',
      '__floatdisf': 'T',
      '__floatsidf': 'T',
      '__floatsisf': 'T',
      '__floatundidf': 'T',
      '__floatundisf': 'T',
      '__floatunsidf': 'T',
      '__floatunsisf': 'T',
      '__gedf2': 'T',
      '__gesf2': 'T',
      '__gnu_ldivmod_helper': 'T',
      '__gnu_uldivmod_helper': 'T',
      '__gtdf2': 'T',
      '__gtsf2': 'T',
      '__ledf2': 'T',
      '__lesf2': 'T',
      '__lshrdi3': 'T',
      '__ltdf2': 'T',
      '__ltsf2': 'T',
      '__muldf3': 'T',
      '__nedf2': 'T',
      '__nesf2': 'T',
      '__subdf3': 'T',
      '__subsf3': 'T',
      '__udivdi3': 'T',
      '__udivsi3': 'T',
      // arm64
      '__clz_tab': 'R',
      '__udivti3': 'T',
      // arm64 && x64
      '__emutls_get_address': 'T',
      '__emutls_register_common': 'T',
      // jit x86
      '__moddi3': 'T',
      '__umoddi3': 'T',
    };
    final Map<String, String> badSymbols = <String, String>{};
    for (final String key in entryMap.keys) {
      if (entryMap[key] != expectedSymbols[key]) {
        badSymbols[key] = entryMap[key]!;
      }
    }
    if (badSymbols.isNotEmpty) {
      print('ERROR: $libFlutter exports the wrong symbols');
      print(' Expected $expectedSymbols');
      print(' Library has $entryMap.');
      failures++;
    } else {
      print('OK: $libFlutter');
    }
  }
  return failures;
}

int _checkLinux(String outPath, String nmPath, Iterable<String> builds) {
  int failures = 0;
  for (final String build in builds) {
    final String libFlutter = p.join(outPath, build, 'libflutter_engine.so');
    if (!File(libFlutter).existsSync()) {
      print('SKIPPING: $libFlutter does not exist.');
      continue;
    }
    final ProcessResult nmResult = Process.runSync(nmPath, <String>['-gUD', libFlutter]);
    if (nmResult.exitCode != 0) {
      print('ERROR: failed to execute "nm -gUD $libFlutter":\n${nmResult.stderr}');
      failures++;
      continue;
    }
    final List<NmEntry> entries = NmEntry.parse(nmResult.stdout as String).toList();
    for (final NmEntry entry in entries) {
      if (entry.type != 'T' && entry.type != 'R') {
        print('ERROR: $libFlutter exports an unexpected symbol type: ($entry)');
        print(' Library has $entries.');
        failures++;
        break;
      }
      if (!(entry.name.startsWith('Flutter')
            || entry.name.startsWith('__Flutter')
            || entry.name.startsWith('kFlutter')
            || entry.name.startsWith('InternalFlutter')
            || entry.name.startsWith('kInternalFlutter'))) {
        print('ERROR: $libFlutter exports an unexpected symbol name: ($entry)');
        print(' Library has $entries.');
        failures++;
        break;
      }
    }
  }
  return failures;
}

class NmEntry {
  NmEntry._(this.address, this.type, this.name);

  final String address;
  final String type;
  final String name;

  static Iterable<NmEntry> parse(String stdout) {
    return LineSplitter.split(stdout).map((String line) {
      final List<String> parts = line.split(' ');
      return NmEntry._(parts[0], parts[1], parts.last);
    });
  }

  @override
  String toString() => '$name: $type';
}
