Pull to refresh

Comments 28

Спасибо за совет! Это будет получше регулярок.

Тут дополню, а разве при использовании метода + writePropertyList:toStream:format:options:error:

у нас порядок элементов не изменится в Info.plist, так как корневой элемент это Dictionary? Тогда, возможно, что регулярка будет всё же лучше.

Какой ещё порядок элементов? У словаря нет понятия "порядок элементов".

В том и дело, что порядка нет. То есть при изменении Info.plist методом + writePropertyList:toStream:format:options:error: сам файл будет полностью меняться. И получится, что каждый раз при сохранении ключи перемешиваются.

Ну и что? Никого же не смущает, что в Dictionary и NSDictionary никогда не было никакого порядка.

Разве что это проблема для тех, кто парсит такие файлы регулярками, но это исправимо.

Про ArgumentParser я упомянул в статье, что если менеджер зависимостей не SPM и подключать его не хочется, то можно воспользоваться уже доступными способами.

но зачем вообще что-то парсить, если есть тот же Environment с именованными параметрами?

URL="https://..."" INFO_PLIST="Info.plist" updater update

Переменные окружения проекта будут доступны, если запустить скрипт из Build Phases секции проекта. А если потребуется запустить скрипт уже из другого места, придется их самом прописывать? Тогда уже лучше выглядит передача параметров.

или же можно использовать UserDefaults: аргументы вида "-arg value" доступны из UserDefaults.standard

updater -arg value

print(UserDefaults.standard.value(forKey: "arg")) -> Optional(value)

Возможность использовать UserDefaults, как я понял, не указана явно в публичной документации. Но способ получше парсинга.

Ещё рекомендую посмотреть man plutil и man PlistBuddy. PlistBuddy почему-то никогда не попадает в $PATH, но он всегда есть в /usr/libexec/PlistBuddy.

Как я понимаю, задача заточена чисто под iOS, и а раз так, то надо и делать всё так, чтобы было максимально удобно для имеющихся инструментов. Например вместо json с самодельным форматом можно было бы сразу хранить нужную ветку Info.plist в формате plist (или json), и делать PlistBuddy -x "Merge remote.plist". Но у PlistBuddy есть фатальный недостаток: он когда видит несуществующий ключ, возвращает ненулевой код ошибки. Это неудобно.

Но можно сделать почти идеально с plutil: plutil -replace 'NSAppTransportSecurity' -json "{ ... }" -- Info.plist

Вот формат, который мы на самом деле хотим видеть в "настроечном" удалённом файле:

{
   "NSExceptionDomains": {
      "dev.app_main.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : true
      },
      "dev.app_quicksearch.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : true
      },
      "dev.app_cached.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : true
      },

      "stage.app_main.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : false
      },
      "stage.app_quicksearch.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : false
      },
      "stage.app_cached.com": {
         "NSExceptionAllowsInsecureHTTPLoads" : false
      }
   }
}

Остаётся только скачать его и обновить ключ в Info.plist используя plutil:

plutil -replace 'NSAppTransportSecurity' -json "$(curl https://raw.githubusercontent.com/Streetmage/DomainsList/main/remote_domains.json)" -- Info.plist

Чтобы очистить ключ:

plutil -replace 'NSAppTransportSecurity' -json '{}' -- Info.plist

Можно было бы делать и plutil -remove 'NSAppTransportSecurity' -- Info.plist, но plutil тоже будет возвращать код ошибки, если ключа не было, это может быть неудобно.

$ plutil -remove 'NSAppTransportSecurity' -- Info.plist
Info.plist: Could not modify plist, error: No value to remove at key path NSAppTransportSecurity
$ echo $?
1

$ plutil -replace 'NSAppTransportSecurity' -json '{}' -- Info.plist
$ echo $?
0

Вместо отдельного "файла-справочника" в приложении, можно читать эти данные прямо из своего же Info.plist. Я подозреваю, что iOS не обидится, если помимо NSExceptionAllowsInsecureHTTPLoads записать ещё и description.

Кажется, так и есть:

print(Bundle.main.object(forInfoDictionaryKey: "NSAppTransportSecurity"))

Optional({
    NSExceptionDomains =     {
        "mydomain.com" =         {
            MyDescription = "Hello World";
            NSExceptionAllowsInsecureHTTPLoads = 1;
        };
    };
})

Но вообще, я не понимаю суть проблемы. Скорее всего это всё находится на подконтрольных серверах, обновляют этот json люди, которые знают и о приложении, и о доменах, и о разработчиках. Info.plist модифицируется при сборке, нужен для отладки. Так может надо просто научить тех, кто меняет домены, пойти в репозиторий приложения и закомитить изменения в Info.plist? Зачем нужна вся эта шайтан-машина со скриптами?

Если вы не хотите, чтобы эти домены попадали в релизную сборку, можно включить в Build Settings настройку Preprocess Info.plist File (INFOPLIST_PREPROCESS), добавить в Info.plist Preprocessor Definitions (INFOPLIST_PREPROCESSOR_DEFINITIONS) строчку типа "DEBUG" для Debug версии, и тогда в Info.plist можно будет сделать так:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
#if DEBUG
  <key>NSAppTransportSecurity</key>
  <dict>
    <key>NSExceptionDomains</key>
    <dict>
      <key>mydomain.com</key>
      <dict>
        <key>NSExceptionAllowsInsecureHTTPLoads</key>
        <true/>
        <key>MyDescription</key>
        <string>Hello World</string>
      </dict>
    </dict>
  </dict>
#endif
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
...

Я сейчас впервые в жизни это попробовал сделать, и у меня заработало. Однако, боюсь, что plutil с макросами в теле файла уже не сможет работать. Но я и склоняю к тому, чтобы это менять руками, такие обновления происходят очень редко, и на мой взгляд лучше бы их было видно в истории изменений.

Пример здесь абстрактный и больше был призван показать сам процесс возможности использования утилиты на Swift-е. Он упрощен специально, чтобы не вносить доп. условий.

В исходной задаче файл json привести сразу к формату plist-а было невозможно, это был сторонний сервис. Плюс был нюанс с тем, что не все поля нужно заменять в массиве, а только те, у которых в названии есть определенный признак, то есть ещё дополнительно предварительно отфильтровать.

На этом этапе я решил, что использование консольных утилит типа PlistBuddy потребует их более глубокого изучения и встанет вопрос поддержки другими iOS разработчиками. Например, выше вроде бы простое решение с утилитами plutil и PlistBuddy всё равно требует знания нюансов и опыта работы с этими утилитами.

А вот ваше решение с тем, чтобы хранить дополнительную инфу сразу в plist и не хранить отдельно JSON интересное. Хотя для нас бы не сработало, потому что массив состоял из строк, а не из словарей, куда можно ещё дополнительно закинуть тот же description.

Хотя для нас бы не сработало, потому что массив состоял из строк, а не из словарей, куда можно ещё дополнительно закинуть тот же description.

Как это понять?

Например, вот так:

<array>
        <string>value_without_filter_tag</string>
		<string>value_with_filter_tag_1</string>
		<string>value_with_filter_tag_2</string>
		<string>value_with_filter_tag_3</string>
<array>

И нужно было оставить всё, что без value_without_filter_tag. А все другие значение с подстрокой "with_filter_tag" заменить.

Я не понимаю, какой массив, из каких строк, и почему что-то нельзя с ним сделать?

Я предлагал читать данные о доменах (видимо для отображения в UI) прямо из Info.plist, причём туда можно записать не только имя домена и флаг NSExceptionAllowsInsecureHTTPLoads, но так же и любые другие пользовательские данные, как тот же description. Да, структура будет чуть другая, ну и что? Вместо массива объектов типа [{ domain, description, flag }] будет структура { domain: { description, flag} }. Попарсить её нет никакой проблемы, зато есть один источник правды.

В рамках доменов и данного примера решение то, что надо, с хранением всего в Info.plist. Но если будет просто массив строк, то всё равно придется хранить отдельный json файл.

Если будет "просто массив строк", то можно его прямо в исходниках и хранить.

Глядя на код в репозитории, я увидел множество проблем с этим примером. Какие-то касаются кода, какие-то более концептуальные. Я даже и не знаю с чего начать.

Начну пожалуй с любимого:

static func fetchDomainsJson(remotePath: String) -> [Domain] {
  guard let url = URL(string: remotePath) else { fatalError(.remotePathIsInvalid) }
  do {
    let data = try Data(contentsOf: url)
    return try JSONDecoder().decode([Domain].self, from: data)
  } catch {
    fatalError(.failedToLoadRemoteFile(error))
  }
}

Обожаю эти do { try f() } catch { fatalError(error) }. Почему бы просто не сделать try! – система точно так же "упадёт", и точно так же напечатает ошибку.

То же касается и url. По-моему, хорошим тоном было бы сразу дать функции нормальные данные (готовый URL), а не заставлять её пытаться из потенциального мусора создать себе нормальное окружение.

Напоследок, раз уж тут есть какие-то try, то пусть бы функция была throws, а делать ли try! пусть решают уровнем выше.

Похожий, но другой пример проблемы:

guard let data = try? Data(contentsOf: url),
			  let infoPlist = String(data: data, encoding: .utf8)
		else { fatalError(.infoPlistNotFound) }

Если do { try f() } catch { fatalError() } и f()! это в принципе одно и то же (потому что одинаково напечатается одинаковая ошибка и одинаково упадёт), то здесь же стало хуже. try! Data(...) напечатал бы гораздо больше полезной информации о том, почему же не получилось прочитать данные, чем просто "infoPlistNotFound". Что ещё и не факт, что правда. Файл-то может быть found, но не смочь прочитаться по тысяче причин: недостаточно прав, прочитался только до половины, да чёрт ещё знает почему. Полезная информация, которая могла бы быть в логах, и помочь расследованию, оказывается просто потеряна.

Итак, вот мой вариант:

import Foundation

let infoPlistURL = URL(fileURLWithPath: UserDefaults.standard.string(forKey: "infoPlist") ?? "Info.plist")

var format: PropertyListSerialization.PropertyListFormat = .xml
var plist = try! PropertyListSerialization
  .propertyList(from: try! Data(contentsOf: infoPlistURL),
                options: .mutableContainersAndLeaves,
                format: &format) as! NSMutableDictionary

let kvPairs = try JSONDecoder()
  .decode([DomainInfo].self, from: try readDomainsData())
  .map {
    ($0.name, [
      "NSExceptionAllowsInsecureHTTPLoads": $0.allowsInsecureHTTPLoads,
      "MyDescription" : $0.description
    ])
  }

plist["NSAppTransportSecurity"] = [
  "NSExceptionDomains": Dictionary(uniqueKeysWithValues: kvPairs)
]

try! PropertyListSerialization
  .data(fromPropertyList: plist, format: format, options: 0)
  .write(to: infoPlistURL)

struct DomainInfo: Decodable {
  let name: String
  let allowsInsecureHTTPLoads: Bool
  let description: String
}

func readDomainsData() throws -> Data {
  if let url = UserDefaults.standard.string(forKey: "from"), url != "-" {
    if url.hasPrefix("http://") || url.hasPrefix("https://") {
      return try Data(contentsOf: URL(string: url)!)
    }
    return try Data(contentsOf: URL(fileURLWithPath: url))
  }
  return FileHandle.standardInput.readDataToEndOfFile()
}
  • мы вроде согласились, что отдельный файл не нужен, поэтому обновляется только Info.plist;

  • Info.plist по дефолту ищется в текущей рабочей директории, или его можно передать аргументом -infoPlist;

  • json с данными может читаться из трёх источников:

    • updater -from https://.... – URL в интернете;

    • updater -from /local/path/on/disk – файл на диске;

    • updater -from - (dash) или же без аргументов – прочитается из stdin.

      • curl url | updater;

      • cat file | updater;

      • updater < file;

      • updater и ввод с завершающим Ctrd+D;

  • команда для удаления тоже не нужна, т.к. это просто "записать пустой массив", и это можно сделать как echo "[]" | updater;

  • сохраняет тот же формат файла Info.plist (xml или бинарный).

Sign up to leave a comment.

Articles