privacy manifest discovery skips build-output and copy folders (hidden dirs like .native-build, build, DerivedData, node_modules, dist, Products, built .app / .xcarchive) in scan, check and privacy-manifest: a stale .native-build copy was checked instead of the real ios/ manifest; test

This commit is contained in:
CRTkafa 2026-10-14 13:14:49 +08:00
parent 7219ded3e8
commit 73db689386
4 changed files with 49 additions and 4 deletions

View file

@ -230,3 +230,14 @@ export function evaluatePrivacyManifest(i: PmInput): PmFinding[] {
const order: Record<PmLevel, number> = { blocker: 0, warn: 1, info: 2, ok: 3 };
return out.sort((a, b) => order[a.level] - order[b.level]);
}
const COPY_DIRS = new Set(['build', 'Build', 'DerivedData', 'node_modules', 'dist', 'Products', 'Intermediates.noindex', 'ArchiveIntermediates']);
/** A PrivacyInfo.xcprivacy path (relative to the project) that is build output or a copy, not the app's own manifest:
* under a hidden folder (.native-build, .output, .build), build/, DerivedData, node_modules, dist, Xcode Products,
* or inside a built .app / .xcarchive. Seen live: a stale .native-build/apps/native/ios copy was checked instead of
* the real apps/native/ios manifest. */
export function isBuildCopyPath(rel: string): boolean {
const dirs = rel.split(/[\\/]/).slice(0, -1);
return dirs.some(d => (d.startsWith('.') && d !== '.' && d !== '..') || COPY_DIRS.has(d) || /\.(app|xcarchive)$/.test(d));
}

View file

@ -6,7 +6,7 @@
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, dirname, extname, join, relative, sep } from 'node:path';
import { resolvePackage } from '../node-packages';
import { type Manifest, parseManifest } from '../privacy-manifest';
import { isBuildCopyPath, type Manifest, parseManifest } from '../privacy-manifest';
import { type DepRef, type Eco, matchesSdk, SDKS, type SdkDef } from './sdks';
export interface Loc { file: string; line: number }
@ -371,7 +371,7 @@ export function detectProject(root: string): ProjectFacts {
} else if (n.endsWith('.entitlements')) {
const { value, lines } = parsePlist(read(f));
for (const [k, v] of Object.entries(value)) setKey(entitlements, k, v, { file: f, line: lines.get(k) ?? 1 });
} else if (n === 'PrivacyInfo.xcprivacy') privacyManifests.push(parseManifest(f, read(f)));
} else if (n === 'PrivacyInfo.xcprivacy') { if (!isBuildCopyPath(f)) privacyManifests.push(parseManifest(f, read(f))); }
else if (n === 'project.pbxproj') {
const t = read(f);
deps.push(...pbxproj(f, t));

View file

@ -21,7 +21,7 @@ import { nativeIosPlugins } from './lib/node-packages';
import { parsePlist, toXmlPlist, type PlistValue } from './lib/plist';
import { manifestFromBytes } from './lib/privacy-files';
import type { AppleDataTypeId, AppleTypeAnswer } from './lib/app-privacy';
import { collectedFromAppPrivacy, evaluatePrivacyManifest, type Manifest, parseManifest, proposeAppManifest, scanSources, type SdkInfo } from './lib/privacy-manifest';
import { collectedFromAppPrivacy, evaluatePrivacyManifest, isBuildCopyPath, type Manifest, parseManifest, proposeAppManifest, scanSources, type SdkInfo } from './lib/privacy-manifest';
const SKIP = new Set(['.git', 'build', 'DerivedData', 'node_modules', '.build', 'xcuserdata']);
const SRC = /\.(swift|m|mm|c|cc|cpp|h|hpp)$/;
@ -105,7 +105,7 @@ export async function run(args: Args): Promise<void> {
return true;
}
const n = basename(p);
if (n === 'PrivacyInfo.xcprivacy') manifests.push(p);
if (n === 'PrivacyInfo.xcprivacy') { if (!isBuildCopyPath(relative(root, p))) manifests.push(p); }
else if (n === 'Info.plist' && !infoPlist && !/Tests?\//.test(p)) infoPlist = readFileSync(p, 'utf8');
else if (SRC.test(n) && statSync(p).size < 2_000_000) sources.push({ path: relative(process.cwd(), p), text: readFileSync(p, 'utf8') });
});