format import { UnsupportedError } from 'flipper-common' ; import adbkit , { Client } from 'adbkit' ; const allowedAppNameRegex = /^[\w.-]+$/ ; const appNotApplicationRegex = /not an application/ ; const appNotDebuggableRegex = /debuggable/ ; const operationNotPermittedRegex = /not permitted/ ; const logTag = 'androidContainerUtility' ; export type AppName = string ; export type Command = string ; export type FilePath = string ; export type FileContent = string ; export async function push ( client , deviceId , app , filepath , contents ) { validateAppName ( app ) ; validateFilePath ( filepath ) ; validateFileContent ( contents ) ; return await _push ( client , deviceId , app , filepath , contents ) ; } export async function pull ( client , deviceId , app , path ) { validateAppName ( app ) ; validateFilePath ( path ) ; return await _pull ( client , deviceId , app , path ) ; } function validateAppName ( app ) { if ( ! app . match ( allowedAppNameRegex ) ) { throw new Error ( `Disallowed run-as user: ${ app }` ) ; } } function validateFilePath ( filePath ) { if ( filePath . match ( /[']/ ) ) { throw new Error ( `Disallowed escaping filepath: ${ filePath }` ) ; } } function validateFileContent ( content ) { if ( content . match ( /["]/ ) ) { throw new Error ( `Disallowed escaping file content: ${ content }` ) ; } } enum RunAsErrorCode { NotAnApp = 1 , NotDebuggable = 2 } class RunAsError extends Error { code : RunAsErrorCode ; constructor ( code , message ? ) { super ( message ) ; this . code = code ; Object . setPrototypeOf ( this , new . target . prototype ) ; } } function _push ( client , deviceId , app , filename , contents ) { console . debug ( `Deploying ${ filename } to ${ deviceId }:${ app }` , logTag ) ; const command = `echo "${ contents }" > '${ filename }' && chmod 644 '${ filename }'` ; return executeCommandAsApp ( client , deviceId , app , command ) . then ( ( _ ) => undefined ) . catch ( ( error ) => { if ( error instanceof RunAsError ) { executeCommandWithSu ( client , deviceId , app , command , error ) ; return undefined ; } throw error ; } ) ; } function _pull ( client , deviceId , app , path ) { const command = `cat '${ path }'` ; return executeCommandAsApp ( client , deviceId , app , command ) . catch ( ( error ) => { if ( error instanceof RunAsError ) { return executeCommandWithSu ( client , deviceId , app , command , error ) ; } throw error ; } ) ; } function executeCommandAsApp ( client , deviceId , app , command ) { return _executeCommandWithRunner ( client , deviceId , app , command , `run-as '${ app }'` ) ; } async function executeCommandWithSu ( client , deviceId , app , command , originalErrorToThrow ) { try { return _executeCommandWithRunner ( client , deviceId , app , command , 'su' ) ; } catch ( e ) { console . debug ( e ) ; throw originalErrorToThrow ; } } function _executeCommandWithRunner ( client , deviceId , app , command , runner ) { return client . shell ( deviceId , `echo '${ command }' | ${ runner }` ) . then ( adbkit . util . readAll ) . then ( ( buffer ) => buffer . toString ( ) ) . then ( ( output ) => { if ( output . match ( appNotApplicationRegex ) ) { throw new RunAsError ( RunAsErrorCode . NotAnApp , `Android package ${ app } is not an application. To use it with Flipper, either run adb as root or add an <application> tag to AndroidManifest.xml` ) ; } if ( output . match ( appNotDebuggableRegex ) ) { throw new RunAsError ( RunAsErrorCode . NotDebuggable , `Android app ${ app } is not debuggable. To use it with Flipper, add android:debuggable="true" to the application section of AndroidManifest.xml` ) ; } if ( output . toLowerCase ( ) . match ( operationNotPermittedRegex ) ) { throw new UnsupportedError ( `Your android device (${ deviceId }) does not support the adb shell run-as command. We're tracking this at https://github.com/facebook/flipper/issues/92` ) ; } return output ; } ) ; }