Skip to content

Merge Info.plist entries structurally so plugins can declare any value type - #377

Open
gwleuverink wants to merge 4 commits into
NativePHP:mainfrom
trailhead-labs:fix/368-plist-value-types
Open

Merge Info.plist entries structurally so plugins can declare any value type#377
gwleuverink wants to merge 4 commits into
NativePHP:mainfrom
trailhead-labs:fix/368-plist-value-types

Conversation

@gwleuverink

Copy link
Copy Markdown
Contributor

Fixes #368.

Thanks to @CodyPChristian for the thorough report. A plugin declaring SKAdNetworkItems (an array of dicts, required by every ad network) aborted native:run ios with a TypeError, because injectPlistEntries built the plist by string concatenation and assumed every value is a string or a list of strings. Tracing that assumption turned up more than the crash:

  • arrays of dicts threw on both the new-key and existing-key paths
  • bools and ints were written as strings; mobile-firebase's FirebaseAppDelegateProxyEnabled: false ships as <string></string> today and only works because Firebase happens to read an empty string as NO
  • merging into a key the base plist already nests (CFBundleURLTypes) landed inside the inner CFBundleURLSchemes array, since the regex stopped at the first nested </array>
  • a dict onto NSAppTransportSecurity, or a bool onto any existing key, was silently ignored
  • a plugin's resources/ios/Info.plist lost its bools and arrays, and nested keys were hoisted to the top level
  • new string values were not escaped, so Photos & video produced a plist Xcode rejects

The fix

PlistDocument reads the plist through the DOM and merges by type: scalars replace, lists union on content, dicts merge key by key, empty arrays and nulls contribute nothing, a type mismatch is replaced by the incoming value. Keys nobody touches keep their original markup byte for byte, and strings are escaped by the DOM.

The compiler opens each plist once and merges every source into it in one pass: the plugin's resources/ios/Info.plist, its info_plist entries, its background modes, and the app-level overrides last so they still win. The regex helpers are gone. Cody's suggestion to emit by type is the direction taken; the regex merge had to go with it, because once plugins can contribute nested structures its non-greedy .*?</array> corrupts them.

This is the second run at the problem. #6 went stale with Shane's review open, and that review doubles as the checklist here: indentation follows depth, bools render as Apple's <true/>, empty arrays stay arrays, integers and reals get their own tags, everything is type-hinted, and the tests assert structure by parsing the output back rather than grepping for key names. Placeholders still only resolve in values, not keys, as before.

Two consequences of the union semantics worth knowing. A list never loses items, so a ${PLACEHOLDER} that changes between builds keeps both values until the scaffold is regenerated, the same way an unregistered plugin's keys already linger. And an app-level override replaces a plugin's scalar but unions with its list.

Verification

Probe plugin manifest, built on the simulator:

"info_plist": {
    "SKAdNetworkItems": [
        { "SKAdNetworkIdentifier": "cstr6suwn9.skadnetwork" },
        { "SKAdNetworkIdentifier": "${PROBE_NETWORK}" }
    ],
    "FirebaseAppDelegateProxyEnabled": false,
    "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true },
    "CFBundleURLTypes": [{ "CFBundleURLName": "probe", "CFBundleURLSchemes": ["probe"] }],
    "NSCameraUsageDescription": "Probe: photos & <video>"
}

plutil -p on the installed bundle's Info.plist:

"SKAdNetworkItems" => [
  0 => { "SKAdNetworkIdentifier" => "cstr6suwn9.skadnetwork" }
  1 => { "SKAdNetworkIdentifier" => "4fzdc2evr5.skadnetwork" }
]
"FirebaseAppDelegateProxyEnabled" => 0
"NSAppTransportSecurity" => {
  "NSAllowsArbitraryLoads" => 1
  "NSAllowsArbitraryLoadsInWebContent" => 1
}
"CFBundleURLTypes" => [ 0 => { app entry }, 1 => { "CFBundleURLName" => "probe" ... } ]
"NSCameraUsageDescription" => "Probe: photos & <video>"

A second build produced the same plist, so nothing duplicates on rebuild. Unit tests pin the merge rules on PlistDocument; compiler tests pin that each source reaches both the device and simulator plists. Suite green.

Decisions for Simon and Shane

Things the trace surfaced that I left alone because they change behaviour beyond this issue:

  • Keys from an unregistered plugin stay in the scaffold plist and ship with the app until native:install --force, and a changed .env value for such a plugin never lands either. Pre-existing on main. Either track plugin-contributed keys and drop the ones no longer declared, or document --force as the way to remove a plugin.
  • compile() returns early unless a plugin has Swift sources, info_plist entries or dependencies, so a plugin whose only iOS data is background_modes, entitlements or a resources/ios/Info.plist file contributes nothing, silently. This PR makes that plist file meaningful for the first time, so the gate bites harder now. Widening it is a few lines; happy to fold it in here.
  • A provider class in plugins() that matches no installed package is accepted without a word, since isPluginAllowed compares strings. A warning at discovery time would turn "my plugin does nothing" into a two-minute fix.
  • The entitlements emitters still build XML by hand, keep the first value for a key that already exists, and do not escape. Add WidgetKit extension target support for plugins #174 rewrites that path onto its own PropertyList codec, so merging both as they stand leaves two plist codecs in the package. Whichever lands second should sit on the other's class; PlistDocument can take the entitlements merge as-is if first wins is not something you want to keep.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS plugin plist injection breaks on arrays of dicts (SKAdNetworkItems), blocking any ad-network plugin

1 participant