(params: {
archivePath: string;
platform: 'ios' | 'android';
expectedRootName?: string;
})
| 22 | } |
| 23 | |
| 24 | export async function resolveTarArchiveRootName(params: { |
| 25 | archivePath: string; |
| 26 | platform: 'ios' | 'android'; |
| 27 | expectedRootName?: string; |
| 28 | }): Promise<string> { |
| 29 | const entriesResult = await runCmd('tar', ['-tf', params.archivePath], { allowFailure: true }); |
| 30 | if (entriesResult.exitCode !== 0) { |
| 31 | throw new AppError('INVALID_ARGS', 'Artifact is not a valid tar archive', { |
| 32 | archivePath: params.archivePath, |
| 33 | stdout: entriesResult.stdout, |
| 34 | stderr: entriesResult.stderr, |
| 35 | exitCode: entriesResult.exitCode, |
| 36 | }); |
| 37 | } |
| 38 | |
| 39 | const entries = entriesResult.stdout |
| 40 | .split(/\r?\n/) |
| 41 | .map((entry) => entry.trim()) |
| 42 | .filter(Boolean); |
| 43 | if (entries.length === 0) { |
| 44 | throw new AppError('INVALID_ARGS', 'Uploaded app bundle archive is empty'); |
| 45 | } |
| 46 | |
| 47 | const normalizedEntries = entries.map(normalizeArchiveEntry); |
| 48 | const rootName = |
| 49 | params.expectedRootName ?? resolveArchiveRootName(normalizedEntries, params.platform); |
| 50 | const hasExpectedRoot = normalizedEntries.some( |
| 51 | (entry) => entry === rootName || entry.startsWith(`${rootName}/`), |
| 52 | ); |
| 53 | if (!hasExpectedRoot) { |
| 54 | throw new AppError( |
| 55 | 'INVALID_ARGS', |
| 56 | `Uploaded archive must contain a top-level "${rootName}" bundle`, |
| 57 | ); |
| 58 | } |
| 59 | |
| 60 | for (const entry of normalizedEntries) { |
| 61 | validateArchiveEntryPath(entry, rootName); |
| 62 | } |
| 63 | |
| 64 | const verboseResult = await runCmd('tar', ['-tvf', params.archivePath]); |
| 65 | for (const line of verboseResult.stdout.split(/\r?\n/).filter(Boolean)) { |
| 66 | if (line[0] === 'l' || line[0] === 'h') { |
| 67 | throw new AppError( |
| 68 | 'INVALID_ARGS', |
| 69 | 'Uploaded app bundle archive cannot contain symlinks or hard links', |
| 70 | ); |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | return rootName; |
| 75 | } |
| 76 | |
| 77 | export async function readZipEntries(archivePath: string): Promise<string[] | null> { |
| 78 | const result = await runCmd('unzip', ['-Z1', archivePath], { allowFailure: true }); |
no test coverage detected
searching dependent graphs…