forked from FifiTheBulldog/ios-settings-urls
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerate-list.js
368 lines (333 loc) · 11.3 KB
/
generate-list.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
"use strict";
// I do solemnly swear that I have never been high in my life.
// Not even when writing this god-awful code.
// Maybe someday I'll have the courage to refactor it so that it makes sense.
/*
Locations in /System/Library/ to check:
- PreferenceBundles: only two of the bundles seem to actually contain
a SettingsSearchManifest.plist, but make sure to scan the entire
directory and examine all subfolders that aren't .lproj or .bundle
(excluding _CodeSignature) for .bundle directories.
- PreferenceManifests: Just one big bundle, AppleAccountSettingsSearch.bundle.
- PreferenceManifestsInternal: Two bundles, AccessibilitySettingsSearch.bundle
for accessibility settings and PreferenceManifests.bundle for
everything else.
- BridgeManifests: for Watch URLs.
Other locations to check:
- NanoPreferenceBundles (contains subfolders, like Applications, to scan as dirs)
- UserNotifications/Bundles/com.apple.cmas.bundle/Info.plist
- PrivateFrameworks/PBBridgeSupport.framework/SettingsSearchManifest.plist
Can't realistically scan without a considerable amount of additional logic:
- /System/Library/UserNotifications/Bundles/com.apple.cmas.bundle/Info.plist
> UNUserNotifiactionCenter.UNDefaultCategories[1].UNCategoryActiions[0].UNActionURL
property is a URL that may or may not work:
prefs:root=NOTIFICATIONS_ID#CMAS_GROUP
- Anything in dyld_shared_cache (which has a few more useful URLs), including placeholders
Output:
- Markdown list (English only?)
- JSON for each localization
- JSON containing all URLs
- JSON (English only) formatted for alombi's site (for now)
*/
const fs = require("fs");
const { basename, extname, join, resolve } = require("path");
const plist = require("simple-plist");
const ROOT_STR = "(root)";
const SSM = "SettingsSearchManifest";
const OVERRIDES_PATH = resolve(".", "overrides.json");
/**
* Path to the iOS simulator in Xcode on macOS.
*/
const SIM_PATH = join(
"/",
"Applications",
"Xcode.app",
"Contents",
"Developer",
"Platforms",
"iPhoneOS.platform",
"Library",
"Developer",
"CoreSimulator",
"Profiles",
"Runtimes",
"iOS.simruntime",
"Contents",
"Resources"
);
// Theoretically we could go through all of /System/Library or even
// the entire filesystem, but that would take a very long time with
// almost no gains.
/**
* Directories in /System/Library/ to scan for Settings URLs.
*/
const DIRS = [
"BridgeManifests",
"NanoPreferenceBundles",
"PreferenceBundles",
"PreferenceManifests",
"PreferenceManifestsInternal"
];
let mainPath = join("/", "System", "Library");
const overrides = require(OVERRIDES_PATH);
let iosVersion = "";
switch (process.platform) {
case "darwin":
// Could be jailbroken or using a-Shell on iOS, or running on macOS
break;
case "linux":
// Assume iSH on iOS
require("child_process").execFileSync("mount", ["-t", "real", "/", "/mnt"]);
mainPath = join("/", "mnt", mainPath);
break;
default:
throw new Error("Unsupported platform: " + process.platform);
}
// Adjust the /System/Library/ path to point to the iOS simulator's filesystem
// on macOS, and determine the iOS version
/**
* Information about the system from SystemVersion.plist, particularly the platform name and version.
* @type {Object.<string, string>}
*/
const systemVersion = plist.readFileSync(join(mainPath, "CoreServices", "SystemVersion.plist"));
switch (systemVersion.ProductName) {
case "Mac OS X":
case "macOS":
mainPath = join(SIM_PATH, "RuntimeRoot", mainPath);
iosVersion = plist.readFileSync(join(SIM_PATH, "profile.plist")).defaultVersionString;
break;
case "iPhone OS":
iosVersion = systemVersion.ProductVersion;
break;
default:
throw new Error("Unsupported platform: " + process.platform);
}
/**
* Strings to localize all of the Settings URLs.
*
* Structure:
* {
* LOCALE_NAME: {
* FILE_ID: {
* LABEL_NAME: STRING
* }
* }
* }
* @type {Object.<string, Object.<string, Object.<string, string>>>}
*/
const locales = {};
/**
* URL items read from manifests.
* @type {UrlItem[]}
*/
const urlItems = [];
/**
* Removes the extension from a file path.
* @param {string} pathName The file path.
* @returns {string} The file path without its extension.
*/
const removeExtension = (pathName) => pathName.substring(0, pathName.lastIndexOf(".")) || pathName;
/**
* Synchronously reads the contents of a directory.
* @param {string} path The path to the directory.
* @returns {fs.Dirent[]} The items in the directory.
*/
const readDirectory = (path) => fs.readdirSync(path, {
withFileTypes: true,
encoding: "utf-8"
});
/**
* An object containing all of the relevant data for a URL dumped from a SettingsSearchManifest.
*/
class UrlItem {
/**
*
* @param {Object.<string, string>} item An item from a SettingsSearchManifest to parse.
* @param {string} manifestPath The full path to the SettingsSearchManifest file.
*/
constructor(item, manifestPath) {
this.label = item.label;
this.url = item.searchURL;
const settingsUrl = new URL(item.searchURL);
const params = new URLSearchParams(settingsUrl.pathname);
this.id = removeExtension(manifestPath);
this.pathComponents = [settingsUrl.protocol, params.get("root")];
const urlPath = params.get("path");
if (urlPath) {
for (const pathPiece of urlPath.split("/")) {
this.pathComponents.push(pathPiece);
}
}
if (settingsUrl.hash) {
this.pathComponents.push(settingsUrl.hash);
}
}
}
/**
* Reads the items from a SettingsSearchManifest file and add them to the global array of URL entries.
* @param {string} manifestPath The path to the SettingsSearchManifest file.
*/
const readManifest = (manifestPath) => {
for (const item of plist.readFileSync(manifestPath).items) {
urlItems.push(new UrlItem(item, manifestPath));
}
}
/**
* Reads the contents of a .lproj directory and add the entries to the global dictionary of locales.
* @param {string} lprojPath The path to the .lproj directory.
*/
const scanLproj = (lprojPath) => {
const localeName = removeExtension(basename(lprojPath));
if (!(localeName in locales)) {
locales[localeName] = {};
}
for (const f of readDirectory(lprojPath)) {
if (!f.isDirectory() && f.name.startsWith(SSM) && extname(f.name) === ".strings") {
// item is a .strings file
let fileId = resolve(lprojPath, "..", removeExtension(f.name));
locales[localeName][fileId] = plist.readFileSync(join(lprojPath, f.name));
}
}
}
/**
* Scans a bundle directory for Settings URLs.
* @param {string} bundlePath The path of the bundle.
*/
const scanBundle = (bundlePath) => {
for (const f of readDirectory(bundlePath)) {
const fullPath = join(bundlePath, f.name);
if (f.isDirectory() && extname(f.name) === ".lproj") {
scanLproj(fullPath); // f is a .lproj folder (contains localizations)
} else if (f.name.startsWith(SSM) && extname(f.name) === ".plist") {
readManifest(fullPath); // f is a manifest (contains URLs)
}
}
}
/**
* Scans a directory for Settings URLs.
* @param {string} dirPath The name of the directory to scan.
*/
const scanDir = (dirPath) => {
for (const f of readDirectory(dirPath)) {
if (f.isDirectory()) {
const fullSubDirPath = join(dirPath, f.name);
if (extname(f.name) === ".bundle") {
scanBundle(fullSubDirPath);
} else if (f.name !== "_CodeSignature") {
scanDir(fullSubDirPath);
}
}
}
}
// Dump all the URLs and localized labels into `locales` and `urlItems`
for (const name of DIRS) {
scanDir(join(mainPath, name));
}
scanBundle(join(mainPath, "PrivateFrameworks", "PBBridgeSupport.framework"));
// Add in overrides
// URL items
for (const item of overrides.items) {
urlItems.push(new UrlItem(item, OVERRIDES_PATH));
}
// Strings
for (const labelId in overrides.strings) {
const labelItem = overrides.strings[labelId];
for (const localeName in labelItem) {
if (!locales[localeName][OVERRIDES_PATH]) {
locales[localeName][OVERRIDES_PATH] = {};
}
locales[localeName][OVERRIDES_PATH] = labelItem[localeName];
}
}
// Sort the URLs into a template list that can then be used to create localized
// JSON and possibly Markdown lists. This list shall have a similar structure
// to what's in the localized versions, but with different keys and a lot more
// data about each URL.
/**
* Adds a UrlItem to the master dictionary.
* @param {UrlItem} urlItem The URL item to insert into the master dictionary of URLs.
* @param {Object.<string, object>} urlsObj The master dictionary of URLs, or a subdictionary of the master dictionary.
* @param {number=} pathIndex The index of urlItem's path components to check.
*/
const addUrlItemToMasterDict = (urlItem, urlsObj, pathIndex = 0) => {
const urlKey = urlItem.pathComponents[pathIndex];
if (pathIndex === urlItem.pathComponents.length - 1) {
// This is the last item in the path, no need for further checks
if (urlKey in urlsObj) {
urlsObj[urlKey].rootItem = urlItem;
} else {
urlsObj[urlKey] = urlItem;
}
return;
}
// We can now assume that this is not the last component of the URL path.
// No need to determine whether it's the last path component or not.
if (urlKey in urlsObj) {
const rootUrlItem = urlsObj[urlKey];
if (rootUrlItem instanceof UrlItem) {
urlsObj[urlKey] = {
rootItem: rootUrlItem,
children: {}
};
}
} else {
urlsObj[urlKey] = {
children: {}
};
}
addUrlItemToMasterDict(urlItem, urlsObj[urlKey].children, pathIndex + 1);
}
const urlsMasterDict = {};
for (const urlItem of urlItems) {
addUrlItemToMasterDict(urlItem, urlsMasterDict);
}
/**/
const defaultLocale = "en";
let locale;
const getLocaleItem = (fileId, labelName) => {
return locales?.[locale]?.[fileId]?.[labelName]
?? locales?.[defaultLocale]?.[fileId]?.[labelName];
}
/**
* Builds a localized object of URL items (for distribution).
* @param {UrlItem|object} obj Object to use as the master dictionary.
*/
const buildLocalizedObject = (item) => {
const result = {};
for (const [key, child] of Object.entries(item)) {
if (child instanceof UrlItem) {
result[getLocaleItem(child.id, child.label)] = child.url;
} else {
const { rootItem } = child;
const deeperResult = {};
if (rootItem) {
deeperResult[ROOT_STR] = rootItem.url;
result[getLocaleItem(rootItem?.id, rootItem?.label)] = deeperResult;
} else {
console.log(child);
throw new Error("Manual addition needed");
}
Object.assign(deeperResult, buildLocalizedObject(child.children));
}
}
return result;
};
const schemes = {
"prefs:": "Settings",
"bridge:": "Watch"
};
/* *
for (const scheme in urlsMasterDict) {
const localizedSubJson = {};
for (const sectionRoot in urlsMasterDict[scheme].children) {
const section = urlsMasterDict[scheme].children[sectionRoot];
const [name, result] = buildLocalizedObject(section, section?.rootItem?.id);
localizedSubJson[name] = result;
}
fs.writeFile(scheme.replace(":", "") + ".json", JSON.stringify(localizedSubJson, null, 2), {encoding: "utf-8"});
}
/* ===== ALL MAIN CODE GOES ABOVE THIS LINE ===== */
/* ===== TEST COMPONENTS BELOW THIS LINE ===== */
console.log(buildLocalizedObject(urlsMasterDict["prefs:"].children))
console.log(Object.keys(locales))
//console.log(urlItems.filter(u => u.url.includes( "prefs:root=APPLE_ACCOUNT")))