diff --git a/packages/targets/pkg-fdroid/src/index.test.ts b/packages/targets/pkg-fdroid/src/index.test.ts index 7dcb5475..5ec2b2c0 100644 --- a/packages/targets/pkg-fdroid/src/index.test.ts +++ b/packages/targets/pkg-fdroid/src/index.test.ts @@ -8,6 +8,7 @@ import adapter from './index.js'; smokeTest(adapter, { idPrefix: 'pkg', requireKind: true }); const tempDirs: string[] = []; +const invalidPackageNames = ['../escape', 'com.acme/app', 'com..acme', '1com.acme.app', 'com.int.app']; afterEach(async () => { await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); @@ -79,6 +80,25 @@ describe('F-Droid target', () => { }); }); + it('rejects invalid package names before writing metadata artifacts', async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-fdroid-')); + tempDirs.push(outDir); + const ctx = fakeBuildContext({ outDir }) as any; + const metadata = { + categories: ['Development'], + license: 'MIT', + sourceRepo: 'https://github.com/acme/app', + }; + + for (const packageName of invalidPackageNames) { + await expect(adapter.build(ctx, { + packageName, + mode: 'main-repo', + metadata, + })).rejects.toThrow('packageName'); + } + }); + it('keeps dry-run shipping side-effect free for main repo submissions', async () => { await expect(adapter.ship(fakeShipContext({ dryRun: true }) as any, { packageName: 'com.acme.app', @@ -96,4 +116,18 @@ describe('F-Droid target', () => { }, }); }); + + it('rejects invalid package names before shipping', async () => { + for (const packageName of invalidPackageNames) { + await expect(adapter.ship(fakeShipContext({ dryRun: true }) as any, { + packageName, + mode: 'main-repo', + metadata: { + categories: ['Development'], + license: 'Apache-2.0', + sourceRepo: 'https://github.com/acme/app', + }, + })).rejects.toThrow('packageName'); + } + }); }); diff --git a/packages/targets/pkg-fdroid/src/index.ts b/packages/targets/pkg-fdroid/src/index.ts index 73092161..44cac235 100644 --- a/packages/targets/pkg-fdroid/src/index.ts +++ b/packages/targets/pkg-fdroid/src/index.ts @@ -77,11 +77,31 @@ function resolveRepoDir(ctx: { projectDir: string }, config: Config): string { return isAbsolute(dir) ? dir : join(ctx.projectDir, dir); } +const JAVA_RESERVED_WORDS = new Set([ + 'abstract', 'assert', 'boolean', 'break', 'byte', 'case', 'catch', 'char', + 'class', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', + 'extends', 'final', 'finally', 'float', 'for', 'goto', 'if', 'implements', + 'import', 'instanceof', 'int', 'interface', 'long', 'native', 'new', + 'package', 'private', 'protected', 'public', 'return', 'short', 'static', + 'strictfp', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', + 'transient', 'try', 'void', 'volatile', 'while', 'true', 'false', 'null', +]); + +function assertPackageName(packageName: string): void { + const segment = '[A-Za-z][A-Za-z0-9_]*'; + const pattern = new RegExp(`^${segment}(\\.${segment})+$`); + const segments = packageName.split('.'); + if (!pattern.test(packageName) || segments.some((part) => JAVA_RESERVED_WORDS.has(part))) { + throw new Error(`packageName must be a valid Android package name, got "${packageName}"`); + } +} + export default defineTarget({ id: 'pkg-fdroid', kind: 'package-manager', label: 'F-Droid (Android FOSS repo)', async build(ctx, config) { + assertPackageName(config.packageName); if (config.mode === 'main-repo') { ctx.log(`render fdroiddata metadata for ${config.packageName}`); const metadataPath = join(ctx.outDir, `${config.packageName}.yml`); @@ -109,6 +129,7 @@ export default defineTarget({ return { artifact: cwd }; }, async ship(ctx, config) { + assertPackageName(config.packageName); if (config.mode === 'main-repo') { ctx.log(`open PR against fdroiddata for ${config.packageName}`); if (ctx.dryRun) {