chromium/ios/chrome/tools/strings/substitute_strings_identifier.mm

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Foundation/Foundation.h>
#import <stdio.h>
#import <unistd.h>

#import <string>

#import "base/apple/foundation_util.h"
#import "base/files/file.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/tools/strings/grit_header_parsing.h"

namespace {

using PList = NSDictionary<NSString*, NSObject*>;

const char kUsageString[] = R"(
usage: substitute_strings_identifier -I header_path -i source_path -i output_path

Loads the plist at `source_path` and replace all string values corresponding
to string identifiers to their numerical values (as found in `header_path`)
and write the resulting plist to `output_path`.
)";

NSObject* ConvertValue(NSObject* value, const ResourceMap& resource_map);

PList* ConvertPlist(PList* plist, const ResourceMap& resource_map) {
  NSMutableDictionary* converted = [[NSMutableDictionary alloc] init];
  for (NSString* key in plist) {
    NSObject* object = ConvertValue(plist[key], resource_map);
    if (!object) {
      return nil;
    }

    converted[key] = object;
  }
  return [converted copy];
}

NSObject* ConvertString(NSString* string, const ResourceMap& resource_map) {
  const std::string key = base::SysNSStringToUTF8(string);
  auto iter = resource_map.find(key);
  if (iter == resource_map.end()) {
    if (base::StartsWith(key, "IDS_") || base::StartsWith(key, "IDR_")) {
      fprintf(stderr, "ERROR: no value found for string: %s\n", key.c_str());
      return nil;
    }

    return string;
  }

  return [NSNumber numberWithInt:iter->second];
}

NSArray* ConvertArray(NSArray* array, const ResourceMap& resource_map) {
  NSMutableArray* converted = [[NSMutableArray alloc] init];
  for (NSObject* value in array) {
    NSObject* object = ConvertValue(value, resource_map);
    if (!object) {
      return nil;
    }

    [converted addObject:object];
  }
  return [converted copy];
}

NSObject* ConvertValue(NSObject* value, const ResourceMap& resource_map) {
  if ([value isKindOfClass:[NSString class]]) {
    NSString* string = base::apple::ObjCCastStrict<NSString>(value);
    return ConvertString(string, resource_map);
  }

  if ([value isKindOfClass:[NSArray class]]) {
    NSArray<NSObject*>* array = base::apple::ObjCCastStrict<NSArray>(value);
    return ConvertArray(array, resource_map);
  }

  if ([value isKindOfClass:[NSDictionary class]]) {
    PList* plist = base::apple::ObjCCastStrict<NSDictionary>(value);
    return ConvertPlist(plist, resource_map);
  }

  return value;
}

bool ConvertFile(const base::FilePath& source_path,
                 const base::FilePath& output_path,
                 const ResourceMap& resource_map) {
  NSURL* source_url = base::apple::FilePathToNSURL(source_path);
  NSURL* output_url = base::apple::FilePathToNSURL(output_path);

  NSError* error = nil;
  PList* source_plist = [NSDictionary dictionaryWithContentsOfURL:source_url
                                                            error:&error];
  if (error) {
    fprintf(stderr, "ERROR: loading %s failed: %s\n",
            source_path.AsUTF8Unsafe().c_str(),
            base::SysNSStringToUTF8(error.localizedDescription).c_str());
    return false;
  }

  PList* output_plist = ConvertPlist(source_plist, resource_map);
  if (!output_plist) {
    return false;
  }

  base::File::Error file_error;
  const base::FilePath output_dir = output_path.DirName();
  if (!base::CreateDirectoryAndGetError(output_dir, &file_error)) {
    fprintf(stderr, "ERROR: creating %s failed: %s\n",
            output_dir.AsUTF8Unsafe().c_str(),
            base::File::ErrorToString(file_error).c_str());
    return false;
  }

  if (![output_plist writeToURL:output_url error:&error]) {
    fprintf(stderr, "ERROR: writing %s failed: %s\n",
            output_path.AsUTF8Unsafe().c_str(),
            base::SysNSStringToUTF8(error.localizedDescription).c_str());
    return false;
  }

  return true;
}

int RealMain(int argc, char* const argv[]) {
  base::FilePath source_path;
  base::FilePath output_path;
  std::vector<base::FilePath> headers;

  int ch = 0;
  while ((ch = getopt(argc, argv, "I:i:o:h")) != -1) {
    switch (ch) {
      case 'I':
        headers.push_back(base::FilePath(optarg));
        break;

      case 'i':
        if (!source_path.empty()) {
          fprintf(stderr, "ERROR: cannot pass -i multiple times\n");
          return 1;
        }

        source_path = base::FilePath(optarg);
        break;

      case 'o':
        if (!output_path.empty()) {
          fprintf(stderr, "ERROR: cannot pass -o multiple times\n");
          return 1;
        }

        output_path = base::FilePath(optarg);
        break;

      case 'h':
        fprintf(stdout, "%s", kUsageString + 1);
        return 0;

      default:
        fprintf(stderr, "ERROR: unknown argument: -%c\n", ch);
        return 1;
    }
  }

  if (headers.empty()) {
    fprintf(stderr, "ERROR: header_path is required.\n");
    return 1;
  }

  if (source_path.empty()) {
    fprintf(stderr, "ERROR: source_path is required.\n");
    return 1;
  }

  if (output_path.empty()) {
    fprintf(stderr, "ERROR: output_path is required.\n");
    return 1;
  }

  std::optional<ResourceMap> resource_map =
      LoadResourcesFromGritHeaders(headers);

  if (!resource_map) {
    return 1;
  }

  if (!ConvertFile(source_path, output_path, *resource_map)) {
    return 1;
  }

  return 0;
}

}  // anonymous namespace

int main(int argc, char* const argv[]) {
  @autoreleasepool {
    return RealMain(argc, argv);
  }
}