From a40acc24baae9946832d6eec50043e6d14997f8e Mon Sep 17 00:00:00 2001 From: Andre Bandarra Date: Thu, 9 Apr 2020 18:22:02 +0100 Subject: [PATCH] Implements input validation for the AplicationId - Implements validation and tests - Improves some error messages on the CLI --- packages/cli/src/lib/cmds/init.ts | 52 ++++++++++++++++++++++---- packages/core/src/index.ts | 2 + packages/core/src/lib/util.ts | 31 +++++++++++++++ packages/core/src/spec/lib/utilSpec.ts | 30 +++++++++++++++ 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/cmds/init.ts b/packages/cli/src/lib/cmds/init.ts index 374603b3..6134ae11 100644 --- a/packages/cli/src/lib/cmds/init.ts +++ b/packages/cli/src/lib/cmds/init.ts @@ -17,7 +17,7 @@ import * as fs from 'fs'; import Color = require('color'); import * as inquirer from 'inquirer'; -import {Config, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest} from '@bubblewrap/core'; +import {Config, JdkHelper, KeyTool, Log, TwaGenerator, TwaManifest, util} from '@bubblewrap/core'; import {validateColor, validatePassword, validateUrl, notEmpty} from '../inputHelpers'; import {ParsedArgs} from 'minimist'; import {APP_NAME} from '../constants'; @@ -36,19 +36,34 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Domain being opened in the TWA:', default: twaManifest.host, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('host cannot be empty'); + }, }, { name: 'name', type: 'input', message: 'Name of the application:', default: twaManifest.name, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('name cannot be empty'); + }, }, { name: 'launcherName', type: 'input', message: 'Name to be shown on the Android Launcher:', default: twaManifest.launcherName, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('Launcher name cannot be empty'); + }, }, { name: 'themeColor', type: 'input', @@ -66,7 +81,12 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Relative path to open the TWA:', default: twaManifest.startUrl, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('URL cannot be empty'); + }, }, { name: 'iconUrl', type: 'input', @@ -91,19 +111,35 @@ async function confirmTwaConfig(twaManifest: TwaManifest): Promise type: 'input', message: 'Android Package Name (or Application ID):', default: twaManifest.packageId, - validate: notEmpty, + validate: async (input): Promise => { + if (!util.validatePackageId(input)) { + throw new Error('Invalid Application Id. Check requiements at ' + + 'https://developer.android.com/studio/build/application-id'); + } + return true; + }, }, { name: 'keyPath', type: 'input', message: 'Location of the Signing Key:', default: twaManifest.signingKey.path, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('KeyStore location cannot be empty'); + }, }, { name: 'keyAlias', type: 'input', message: 'Key name:', default: twaManifest.signingKey.alias, - validate: notEmpty, + validate: async (input): Promise => { + if (notEmpty(input)) { + return true; + } + throw new Error('Key alias cannot be empty'); + }, }, ]); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 28b5377b..29ac8f5f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,6 +22,7 @@ import {JdkHelper} from './lib/jdk/JdkHelper'; import {KeyTool} from './lib/jdk/KeyTool'; import {TwaManifest} from './lib/TwaManifest'; import {TwaGenerator} from './lib/TwaGenerator'; +import * as util from './lib/util'; export {AndroidSdkTools, Config, @@ -31,4 +32,5 @@ export {AndroidSdkTools, Log, TwaGenerator, TwaManifest, + util, }; diff --git a/packages/core/src/lib/util.ts b/packages/core/src/lib/util.ts index e9e2df5f..5a6bc97a 100644 --- a/packages/core/src/lib/util.ts +++ b/packages/core/src/lib/util.ts @@ -28,6 +28,7 @@ const extractZipPromise = promisify(extractZip); // Regex for disallowed characters on Android Packages, as per // https://developer.android.com/guide/topics/manifest/manifest-element.html#package const DISALLOWED_ANDROID_PACKAGE_CHARS_REGEX = /[^a-zA-Z0-9_\.]/g; +const VALID_PACKAGE_ID_SEGMENT_REGEX = /^[a-zA-Z][A-Za-z0-9_]*$/; export async function execute(cmd: string[], env: NodeJS.ProcessEnv): Promise { await execPromise(cmd.join(' '), {env: env}); @@ -134,3 +135,33 @@ export function generatePackageId(host: string): string { parts.push('twa'); return parts.join('.').replace(DISALLOWED_ANDROID_PACKAGE_CHARS_REGEX, '_'); } + +/** + * Validates a Package Id, according to the documentation at: + * https://developer.android.com/studio/build/application-id + * + * Rules summary for the Package Id: + * - It must have at least two segments (one or more dots). + * - Each segment must start with a leter. + * - All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. + * + * @param {string} input the package name to be validated + */ +export function validatePackageId(input: string): boolean { + if (input.length <= 0) { + return false; + } + + const parts = input.split('.'); + if (parts.length < 2) { + return false; + } + + for (const part of parts) { + if (part.match(VALID_PACKAGE_ID_SEGMENT_REGEX) === null) { + return false; + } + } + + return true; +} diff --git a/packages/core/src/spec/lib/utilSpec.ts b/packages/core/src/spec/lib/utilSpec.ts index f709ab77..d4b3af1b 100644 --- a/packages/core/src/spec/lib/utilSpec.ts +++ b/packages/core/src/spec/lib/utilSpec.ts @@ -137,4 +137,34 @@ describe('util', () => { expect(result).toBe('com.appspot.pwa_directory_test.twa'); }); }); + + describe('#validatePackageId', () => { + it('returns true for valid packages', () => { + expect(util.validatePackageId('com.pwa_directory.appspot.com')).toBeTrue(); + expect(util.validatePackageId('com.pwa1directory.appspot.com')).toBeTrue(); + }); + + it('returns false for packages with invalid characters', () => { + expect(util.validatePackageId('com.pwa-directory.appspot.com')).toBeFalse(); + expect(util.validatePackageId('com.pwa@directory.appspot.com')).toBeFalse(); + expect(util.validatePackageId('com.pwa*directory.appspot.com')).toBeFalse(); + }); + + it('returns false for packages empty sections', () => { + expect(util.validatePackageId('com.example.')).toBeFalse(); + expect(util.validatePackageId('.com.example')).toBeFalse(); + expect(util.validatePackageId('com..example')).toBeFalse(); + }); + + it('packages with less than 2 sections return false', () => { + expect(util.validatePackageId('com')).toBeFalse(); + expect(util.validatePackageId('')).toBeFalse(); + }); + + it('packages starting with non-letters return false', () => { + expect(util.validatePackageId('com.1char.twa')).toBeFalse(); + expect(util.validatePackageId('1com.char.twa')).toBeFalse(); + expect(util.validatePackageId('com.char.1twa')).toBeFalse(); + }); + }); });