Repository: GoogleChromeLabs/wadb
Branch: main
Commit: 3488a4ab05b0
Files: 73
Total size: 251.8 KB
Directory structure:
gitextract_qi4f3tty/
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── demo/
│ ├── .firebaserc
│ ├── .gitignore
│ ├── firebase.json
│ ├── package.json
│ ├── public/
│ │ ├── 404.html
│ │ ├── index.html
│ │ ├── interactiveshell.html
│ │ ├── js/
│ │ │ ├── interactiveshell.bundle.js
│ │ │ ├── interactiveshell.d.ts
│ │ │ ├── interactiveshell.js
│ │ │ ├── livestream.bundle.js
│ │ │ ├── livestream.d.ts
│ │ │ ├── livestream.js
│ │ │ ├── screenrecord.bundle.js
│ │ │ ├── screenrecord.d.ts
│ │ │ └── screenrecord.js
│ │ ├── livestream.html
│ │ ├── screenrecord.html
│ │ ├── sw.js
│ │ ├── video.html
│ │ ├── workbox-69b5a3b7.js
│ │ └── workbox-aa2f3006.js
│ ├── src/
│ │ ├── interactiveshell.ts
│ │ ├── livestream.ts
│ │ └── screenrecord.ts
│ ├── tsconfig.json
│ ├── webpack.config.js
│ └── workbox-config.js
├── eslint.config.mjs
├── jasmine.json
├── package.json
├── src/
│ ├── index.ts
│ ├── lib/
│ │ ├── AdbClient.ts
│ │ ├── AdbConnectionInformation.ts
│ │ ├── Framebuffer.ts
│ │ ├── Helpers.ts
│ │ ├── IndexedDbKeyStore.ts
│ │ ├── KeyStore.ts
│ │ ├── Log.ts
│ │ ├── Options.ts
│ │ ├── Queues.ts
│ │ ├── Shell.ts
│ │ ├── Stream.ts
│ │ ├── SyncFrame.ts
│ │ ├── message/
│ │ │ ├── Message.ts
│ │ │ ├── MessageChannel.ts
│ │ │ ├── MessageHeader.ts
│ │ │ ├── MessageListener.ts
│ │ │ └── index.ts
│ │ └── transport/
│ │ ├── Transport.ts
│ │ ├── WebUsbTransport.ts
│ │ └── index.ts
│ └── spec/
│ ├── AdbClientSpec.ts
│ ├── IndexedDbKeyStoreSpec.ts
│ ├── QueuesSpec.ts
│ ├── StreamSpec.ts
│ ├── SyncFrameSpec.ts
│ ├── data/
│ │ └── messages/
│ │ ├── connect/
│ │ │ ├── connect_auth_public_key.json
│ │ │ └── connect_simple.json
│ │ └── stream/
│ │ └── open.json
│ ├── message/
│ │ ├── MessageChannelSpec.ts
│ │ ├── MessageHeaderSpec.ts
│ │ └── MessageSpec.ts
│ └── mock/
│ ├── MockKeyStore.ts
│ ├── MockMessageListener.ts
│ └── MockTransport.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module"
},
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"no-trailing-spaces": "error",
"eol-last": "error"
},
"env": {
"node": true,
"jasmine": true
}
}
================================================
FILE: .gitignore
================================================
node_modules/
dist/
.DS_Store
wq
================================================
FILE: .npmignore
================================================
tsconfig.json
src
================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# An ADB Implementation using WebUSB
This project is a TypeScript implementation of the Android Debug Bridge(ADB) protocol over WebUSB.
The implementation inspired on the [webadb.js][1], with the main difference being that
implementation supports multiple concurrent streams.
This is not an exhaustive implementation of the protocol and hasn't been tested on a wide range of
devices.
A non-exhaustive list of things that are not implemented:
- `STAT`: reads stats from the Android filesystem (file size, mode and time).
## Usage
### Connecting to a device
```typescript
const options: Options = {
debug: true,
useChecksum: false,
dump: false,
keySize: 2048,
};
const transport = await WebUsbTransport.open(options);
const adbClient = new AdbClient(transport, options, keyStore);
await adbClient.connect();
```
### Downloading a file from the device (adb pull)
```typescript
const result: Blob = await adbClient.pull('/sdcard/my-video.mp4');
```
### Sending shell commands
```typescript
const result: string = await adbClient.shell('uname -a');
```
### Interactive shell
```typescript
const callback = (output: string) => {
console.log('server: ' + output);
};
const shell: Shell = await adbClient.interactiveShell(callback);
await shell.write('ls /sdcard\n');
await shell.close();
```
## Related Documents
- https://github.com/webadb/webadb.js
- https://github.com/cstyan/adbDocumentation
- https://android.googlesource.com/platform/system/core/+/master/adb/
## Contributing
See [CONTRIBUTING](./CONTRIBUTING.md) for more.
## License
See [LICENSE](./LICENSE) for more.
## Disclaimer
This is not a Google product.
[1]: https://github.com/webadb/webadb.js
================================================
FILE: demo/.firebaserc
================================================
{
"projects": {
"default": "screenrecord-bandarra-me"
}
}
================================================
FILE: demo/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
# Firebase cache
.firebase/
# Firebase config
# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
================================================
FILE: demo/firebase.json
================================================
{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
================================================
FILE: demo/package.json
================================================
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"build": "tsc",
"dev": "webpack serve --mode=development",
"package": "webpack --mode=production && workbox generateSW",
"serve": "serve"
},
"author": "André Cipriani Bandarra",
"license": "Apache-2.0",
"devDependencies": {
"serve": "^14.2.5",
"ts-loader": "^9.5.4",
"typescript": "^5.9.3",
"webpack": "^5.105.2",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.3",
"workbox-cli": "^7.4.0"
},
"dependencies": {
"wadb": "file:../"
}
}
================================================
FILE: demo/public/404.html
================================================
Page Not Found
404
Page Not Found
The specified file was not found on this website. Please check the URL for mistakes and try again.
Why am I seeing this?
This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.
================================================
FILE: demo/public/workbox-69b5a3b7.js
================================================
define("./workbox-69b5a3b7.js",["exports"],(function(e){"use strict";try{self["workbox:core:5.1.4"]&&_()}catch(e){}const t={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},n=e=>[t.prefix,e,t.suffix].filter(e=>e&&e.length>0).join("-"),s=e=>e||n(t.precache),i=e=>new URL(String(e),location.href).href.replace(new RegExp("^"+location.origin),""),c=(e,...t)=>{let n=e;return t.length>0&&(n+=" :: "+JSON.stringify(t)),n};class o extends Error{constructor(e,t){super(c(e,t)),this.name=e,this.details=t}}const r=new Set;const a=(e,t)=>e.filter(e=>t in e),u=async({request:e,mode:t,plugins:n=[]})=>{const s=a(n,"cacheKeyWillBeUsed");let i=e;for(const e of s)i=await e.cacheKeyWillBeUsed.call(e,{mode:t,request:i}),"string"==typeof i&&(i=new Request(i));return i},l=async({cacheName:e,request:t,event:n,matchOptions:s,plugins:i=[]})=>{const c=await self.caches.open(e),o=await u({plugins:i,request:t,mode:"read"});let r=await c.match(o,s);for(const t of i)if("cachedResponseWillBeUsed"in t){const i=t.cachedResponseWillBeUsed;r=await i.call(t,{cacheName:e,event:n,matchOptions:s,cachedResponse:r,request:o})}return r},h=async({cacheName:e,request:t,response:n,event:s,plugins:c=[],matchOptions:h})=>{const f=await u({plugins:c,request:t,mode:"write"});if(!n)throw new o("cache-put-with-no-response",{url:i(f.url)});const w=await(async({request:e,response:t,event:n,plugins:s=[]})=>{let i=t,c=!1;for(const t of s)if("cacheWillUpdate"in t){c=!0;const s=t.cacheWillUpdate;if(i=await s.call(t,{request:e,response:i,event:n}),!i)break}return c||(i=i&&200===i.status?i:void 0),i||null})({event:s,plugins:c,response:n,request:f});if(!w)return;const d=await self.caches.open(e),p=a(c,"cacheDidUpdate"),y=p.length>0?await l({cacheName:e,matchOptions:h,request:f}):null;try{await d.put(f,w)}catch(e){throw"QuotaExceededError"===e.name&&await async function(){for(const e of r)await e()}(),e}for(const t of p)await t.cacheDidUpdate.call(t,{cacheName:e,event:s,oldResponse:y,newResponse:w,request:f})},f=async({request:e,fetchOptions:t,event:n,plugins:s=[]})=>{if("string"==typeof e&&(e=new Request(e)),n instanceof FetchEvent&&n.preloadResponse){const e=await n.preloadResponse;if(e)return e}const i=a(s,"fetchDidFail"),c=i.length>0?e.clone():null;try{for(const t of s)if("requestWillFetch"in t){const s=t.requestWillFetch,i=e.clone();e=await s.call(t,{request:i,event:n})}}catch(e){throw new o("plugin-error-request-will-fetch",{thrownError:e})}const r=e.clone();try{let i;i="navigate"===e.mode?await fetch(e):await fetch(e,t);for(const e of s)"fetchDidSucceed"in e&&(i=await e.fetchDidSucceed.call(e,{event:n,request:r,response:i}));return i}catch(e){for(const t of i)await t.fetchDidFail.call(t,{error:e,event:n,originalRequest:c.clone(),request:r.clone()});throw e}};let w;async function d(e,t){const n=e.clone(),s={headers:new Headers(n.headers),status:n.status,statusText:n.statusText},i=t?t(s):s,c=function(){if(void 0===w){const e=new Response("");if("body"in e)try{new Response(e.body),w=!0}catch(e){w=!1}w=!1}return w}()?n.body:await n.blob();return new Response(c,i)}try{self["workbox:precaching:5.1.4"]&&_()}catch(e){}function p(e){if(!e)throw new o("add-to-cache-list-unexpected-type",{entry:e});if("string"==typeof e){const t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}const{revision:t,url:n}=e;if(!n)throw new o("add-to-cache-list-unexpected-type",{entry:e});if(!t){const e=new URL(n,location.href);return{cacheKey:e.href,url:e.href}}const s=new URL(n,location.href),i=new URL(n,location.href);return s.searchParams.set("__WB_REVISION__",t),{cacheKey:s.href,url:i.href}}class y{constructor(e){this.t=s(e),this.s=new Map,this.i=new Map,this.o=new Map}addToCacheList(e){const t=[];for(const n of e){"string"==typeof n?t.push(n):n&&void 0===n.revision&&t.push(n.url);const{cacheKey:e,url:s}=p(n),i="string"!=typeof n&&n.revision?"reload":"default";if(this.s.has(s)&&this.s.get(s)!==e)throw new o("add-to-cache-list-conflicting-entries",{firstEntry:this.s.get(s),secondEntry:e});if("string"!=typeof n&&n.integrity){if(this.o.has(e)&&this.o.get(e)!==n.integrity)throw new o("add-to-cache-list-conflicting-integrities",{url:s});this.o.set(e,n.integrity)}if(this.s.set(s,e),this.i.set(s,i),t.length>0){const e=`Workbox is precaching URLs without revision info: ${t.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}async install({event:e,plugins:t}={}){const n=[],s=[],i=await self.caches.open(this.t),c=await i.keys(),o=new Set(c.map(e=>e.url));for(const[e,t]of this.s)o.has(t)?s.push(e):n.push({cacheKey:t,url:e});const r=n.map(({cacheKey:n,url:s})=>{const i=this.o.get(n),c=this.i.get(s);return this.u({cacheKey:n,cacheMode:c,event:e,integrity:i,plugins:t,url:s})});return await Promise.all(r),{updatedURLs:n.map(e=>e.url),notUpdatedURLs:s}}async activate(){const e=await self.caches.open(this.t),t=await e.keys(),n=new Set(this.s.values()),s=[];for(const i of t)n.has(i.url)||(await e.delete(i),s.push(i.url));return{deletedURLs:s}}async u({cacheKey:e,url:t,cacheMode:n,event:s,plugins:i,integrity:c}){const r=new Request(t,{integrity:c,cache:n,credentials:"same-origin"});let a,u=await f({event:s,plugins:i,request:r});for(const e of i||[])"cacheWillUpdate"in e&&(a=e);if(!(a?await a.cacheWillUpdate({event:s,request:r,response:u}):u.status<400))throw new o("bad-precaching-response",{url:t,status:u.status});u.redirected&&(u=await d(u)),await h({event:s,plugins:i,response:u,request:e===t?r:new Request(e),cacheName:this.t,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.s}getCachedURLs(){return[...this.s.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this.s.get(t.href)}async matchPrecache(e){const t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n){return(await self.caches.open(this.t)).match(n)}}createHandler(e=!0){return async({request:t})=>{try{const e=await this.matchPrecache(t);if(e)return e;throw new o("missing-precache-entry",{cacheName:this.t,url:t instanceof Request?t.url:t})}catch(n){if(e)return fetch(t);throw n}}}createHandlerBoundToURL(e,t=!0){if(!this.getCacheKeyForURL(e))throw new o("non-precached-url",{url:e});const n=this.createHandler(t),s=new Request(e);return()=>n({request:s})}}let g;const R=()=>(g||(g=new y),g);const q=(e,t)=>{const n=R().getURLsToCacheKeys();for(const s of function*(e,{ignoreURLParametersMatching:t,directoryIndex:n,cleanURLs:s,urlManipulation:i}={}){const c=new URL(e,location.href);c.hash="",yield c.href;const o=function(e,t=[]){for(const n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}(c,t);if(yield o.href,n&&o.pathname.endsWith("/")){const e=new URL(o.href);e.pathname+=n,yield e.href}if(s){const e=new URL(o.href);e.pathname+=".html",yield e.href}if(i){const e=i({url:c});for(const t of e)yield t.href}}(e,t)){const e=n.get(s);if(e)return e}};let U=!1;function m(e){U||((({ignoreURLParametersMatching:e=[/^utm_/],directoryIndex:t="index.html",cleanURLs:n=!0,urlManipulation:i}={})=>{const c=s();self.addEventListener("fetch",s=>{const o=q(s.request.url,{cleanURLs:n,directoryIndex:t,ignoreURLParametersMatching:e,urlManipulation:i});if(!o)return;let r=self.caches.open(c).then(e=>e.match(o)).then(e=>e||fetch(o));s.respondWith(r)})})(e),U=!0)}const v=[],L={get:()=>v,add(e){v.push(...e)}},x=e=>{const t=R(),n=L.get();e.waitUntil(t.install({event:e,plugins:n}).catch(e=>{throw e}))},K=e=>{const t=R();e.waitUntil(t.activate())};e.precacheAndRoute=function(e,t){!function(e){R().addToCacheList(e),e.length>0&&(self.addEventListener("install",x),self.addEventListener("activate",K))}(e),m(t)}}));
//# sourceMappingURL=workbox-69b5a3b7.js.map
================================================
FILE: demo/public/workbox-aa2f3006.js
================================================
define(["exports"],function(t){"use strict";try{self["workbox:core:7.3.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.3.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super(({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)},e,s)}}class o{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)})}addCacheListener(){self.addEventListener("message",t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map(e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})}));t.waitUntil(s),t.ports&&t.ports[0]&&s.then(()=>t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let o=r&&r.handler;const c=t.method;if(!o&&this.i.has(c)&&(o=this.i.get(c)),!o)return;let a;try{a=o.handle({url:s,request:t,event:e,params:i})}catch(t){a=Promise.reject(t)}const h=r&&r.catchHandler;return a instanceof Promise&&(this.o||h)&&(a=a.catch(async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n})),a}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const o=r.match({url:t,sameOrigin:e,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&0===i.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let c;const a={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},h=t=>[a.prefix,t,a.suffix].filter(t=>t&&t.length>0).join("-"),u=t=>t||h(a.precache),l=t=>t||h(a.runtime);function f(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:7.3.0"]&&_()}catch(t){}function w(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),r=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:r.href}}class d{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class p{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.h.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.h=t}}let y;async function g(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=e?e(r):r,c=function(){if(void 0===y){const t=new Response("");if("body"in t)try{new Response(t.body),y=!0}catch(t){y=!1}y=!1}return y}()?i.body:await i.blob();return new Response(c,o)}function R(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class m{constructor(){this.promise=new Promise((t,e)=>{this.resolve=t,this.reject=e})}}const v=new Set;try{self["workbox:strategies:7.3.0"]&&_()}catch(t){}function q(t){return"string"==typeof t?new Request(t):t}class U{constructor(t,e){this.u={},Object.assign(this,e),this.event=e.event,this.l=t,this.p=new m,this.R=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.p.promise)}async fetch(t){const{event:e}=this;let n=q(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.l.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=q(t);let s;const{cacheName:n,matchOptions:i}=this.l,r=await this.getCacheKey(e,"read"),o=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,o);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=q(t);var i;await(i=0,new Promise(t=>setTimeout(t,i)));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(o=r.url,new URL(String(o),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var o;const c=await this.q(e);if(!c)return!1;const{cacheName:a,matchOptions:h}=this.l,u=await self.caches.open(a),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=R(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),o=await t.keys(e,r);for(const e of o)if(i===R(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?c.clone():c)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of v)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:a,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.u[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=q(await t({mode:e,request:n,event:this.event,params:this.params}));this.u[s]=n}return this.u[s]}hasCallback(t){for(const e of this.l.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.l.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.R.push(t),t}async doneWaiting(){for(;this.R.length;){const t=this.R.splice(0),e=(await Promise.allSettled(t)).find(t=>"rejected"===t.status);if(e)throw e.reason}}destroy(){this.p.resolve(null)}async q(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class L{constructor(t={}){this.cacheName=l(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new U(this,{event:e,request:s,params:n}),r=this.U(i,s,e);return[r,this.L(r,i,s,e)]}async U(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this._(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async L(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}class b extends L{constructor(t={}){t.cacheName=u(t.cacheName),super(t),this.C=!1!==t.fallbackToNetwork,this.plugins.push(b.copyRedirectedCacheableResponsesPlugin)}async _(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.O(t,e):await this.N(t,e))}async N(t,e){let n;const i=e.params||{};if(!this.C)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,o=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&o&&"no-cors"!==t.mode&&(this.j(),await e.cachePut(t,n.clone()))}return n}async O(t,e){this.j();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}j(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==b.copyRedirectedCacheableResponsesPlugin&&(n===b.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(b.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}b.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},b.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await g(t):t};class C{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.k=new Map,this.K=new Map,this.P=new Map,this.l=new b({cacheName:u(t),plugins:[...e,new p({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.l}precache(t){this.addToCacheList(t),this.T||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.T=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=w(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.k.has(i)&&this.k.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.k.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.P.has(t)&&this.P.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.P.set(t,n.integrity)}if(this.k.set(i,t),this.K.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return f(t,async()=>{const e=new d;this.strategy.plugins.push(e);for(const[e,s]of this.k){const n=this.P.get(s),i=this.K.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}})}activate(t){return f(t,async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.k.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}})}getURLsToCacheKeys(){return this.k}getCachedURLs(){return[...this.k.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.k.get(e.href)}getIntegrityForCacheKey(t){return this.P.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}let E;const O=()=>(E||(E=new C),E);class x extends i{constructor(t,e){super(({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const o=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some(t=>t.test(s))&&t.searchParams.delete(s);return t}(r,e);if(yield o.href,s&&o.pathname.endsWith("/")){const t=new URL(o.href);t.pathname+=s,yield t.href}if(n){const t=new URL(o.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}},t.strategy)}}function N(t){const e=O();!function(t,e,n){let a;if("string"==typeof t){const s=new URL(t,location.href);a=new i(({url:t})=>t.href===s.href,e,n)}else if(t instanceof RegExp)a=new r(t,e,n);else if("function"==typeof t)a=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});a=t}(c||(c=new o,c.addFetchListener(),c.addCacheListener()),c).registerRoute(a)}(new x(e,t))}t.precacheAndRoute=function(t,e){!function(t){O().precache(t)}(t),N(e)}});
//# sourceMappingURL=workbox-aa2f3006.js.map
================================================
FILE: demo/src/interactiveshell.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AdbClient, IndexedDbKeyStore, Options, Shell, WebUsbTransport} from 'wadb';
const connectButton = document.querySelector('#connect')!;
const disconnectButton = document.querySelector('#disconnect')!;
const output = document.querySelector('#output')!;
const input = (document.querySelector('#input') as HTMLInputElement)!;
const options: Options = {
debug: true,
useChecksum: false,
dump: false,
keySize: 2048,
};
const keyStore = new IndexedDbKeyStore();
let transport: WebUsbTransport | null = null;
let adbClient: AdbClient | null = null;
let shell: Shell | null = null;
function appendToCode(text: string) {
const span = document.createElement('span');
span.innerText = text;
output.appendChild(span);
output.scrollTop = output.scrollHeight;
}
function sendCommand(cmd: string) {
shell!.write(cmd + '\n');
}
connectButton.addEventListener('click', async (_e) => {
try {
transport = await WebUsbTransport.open(options);
adbClient = new AdbClient(transport, options, keyStore);
await adbClient.connect();
shell = await adbClient.interactiveShell(appendToCode);
disconnectButton.classList.toggle('hidden');
connectButton.classList.toggle('hidden');
} catch(e) {
console.error('Connection Failed: ', e);
}
});
disconnectButton.addEventListener('click', async (_e) => {
try {
await shell?.close();
await transport?.close();
transport = null;
adbClient = null;
shell = null;
} catch (e) {
console.error('Error closing the connection', e);
}
disconnectButton.classList.toggle('hidden');
connectButton.classList.toggle('hidden');
});
input.addEventListener('keyup', (e) => {
if (e.keyCode === 13) {
e.preventDefault();
sendCommand(input.value);
input.value = '';
return false;
}
return true;
});
================================================
FILE: demo/src/livestream.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {WebUsbTransport, AdbClient, Options, IndexedDbKeyStore, Stream} from 'wadb';
let transport: WebUsbTransport | null;
let adbClient: AdbClient | null;
const connectButton = document.querySelector('#connect')!;
const disconnectButton = document.querySelector('#disconnect')!;
const startButton = document.querySelector('#start')!;
const stopButton = document.querySelector('#stop')!;
const video: HTMLVideoElement = (document.querySelector('#video') as HTMLVideoElement)!;
const download = (document.querySelector('#download') as HTMLAnchorElement)!;
const status = document.querySelector('#status')!;
const options: Options = {
debug: true,
useChecksum: false,
dump: false,
keySize: 2048,
};
const keyStore = new IndexedDbKeyStore();
connectButton.addEventListener('click', async (_event) => {
try {
transport = await WebUsbTransport.open(options);
adbClient = new AdbClient(transport, options, keyStore);
status.textContent = 'Accept prompt on device';
const adbConnectionInformation = await adbClient.connect();
status.textContent = 'Connected and ready';
console.log('Connected: ', adbConnectionInformation);
connectButton.classList.toggle('hidden');
disconnectButton.classList.toggle('hidden');
} catch(e) {
console.error('Connection Failed: ', e);
status.textContent = 'Failed to connect to a device';
}
});
disconnectButton.addEventListener('click', async (_event) => {
try {
if (adbClient) {
try {
await adbClient.disconnect();
} catch (e) {
console.log('Error disconnecting ADB Client: ', e);
}
adbClient = null;
}
if (transport) {
await transport.close();
transport = null;
}
connectButton.classList.toggle('hidden');
disconnectButton.classList.toggle('hidden');
status.textContent = 'Connect to a device to start';
} catch(e) {
console.error('Disconnecting Failed: ', e);
}
});
let shell: Stream | null = null;
startButton.addEventListener('click', async() => {
// const mediaSource = new MediaSource();
// const url = URL.createObjectURL(mediaSource);
// video.src = url;
status.textContent = 'Recording...';
stopButton.classList.toggle('hidden');
startButton.classList.toggle('hidden');
const textDecoder = new TextDecoder();
// mediaSource.addEventListener('sourceopen', async () => {
shell = await Stream.open(adbClient!, 'exec:screenrecord --output-format=h264 -', options);
// const audioSourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="mp4a.40.2"');
const chunks: Uint8Array[] = [];
let i = 0;
let msg;
while (true) {
console.log(++i);
msg = await shell!.read();
await shell!.write('OKAY');
if (msg.header.cmd === 'CLSE') {
break;
}
console.log(textDecoder.decode(msg.data!.buffer));
chunks.push(new Uint8Array(msg.data!.buffer as ArrayBuffer));
}
console.log(chunks.length);
const objectUrl = URL.createObjectURL(new Blob(chunks));
video.src = objectUrl;
download.href = objectUrl;
// });
});
stopButton.addEventListener('click', async() => {
await shell?.write('CLSE');
});
================================================
FILE: demo/src/screenrecord.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AdbClient, WebUsbTransport, Options, IndexedDbKeyStore, Stream} from 'wadb';
let transport: WebUsbTransport | null;
let adbClient: AdbClient | null;
const connectButton = document.querySelector('#connect')!;
const disconnectButton = document.querySelector('#disconnect')!;
const startButton = (document.querySelector('#start') as HTMLButtonElement)!;
const stopButton = (document.querySelector('#stop') as HTMLButtonElement)!;
const screenshotButton = (document.querySelector('#screencapture') as HTMLButtonElement)!;
const video: HTMLVideoElement = (document.querySelector('#video') as HTMLVideoElement)!;
const screenshot = (document.querySelector('#screenshot') as HTMLImageElement)!;
const download = (document.querySelector('#download') as HTMLAnchorElement)!;
const status = document.querySelector('#status')!;
const options: Options = {
debug: true,
useChecksum: false,
dump: false,
keySize: 2048,
};
const keyStore = new IndexedDbKeyStore();
connectButton.addEventListener('click', async (_event) => {
try {
transport = await WebUsbTransport.open(options);
adbClient = new AdbClient(transport, options, keyStore);
status.textContent = 'Accept prompt on device';
const adbConnectionInformation = await adbClient.connect();
status.textContent = 'Connected and ready';
console.log('Connected: ', adbConnectionInformation);
connectButton.classList.toggle('hidden');
disconnectButton.classList.toggle('hidden');
startButton.removeAttribute('disabled');
screenshotButton.removeAttribute('disabled');
stopButton.removeAttribute('disabled');
} catch(e) {
console.error('Connection Failed: ', e);
status.textContent = 'Failed to connect to a device';
}
});
disconnectButton.addEventListener('click', async (_event) => {
try {
if (adbClient) {
try {
await adbClient.disconnect();
} catch (e) {
console.log('Error disconnecting ADB Client: ', e);
}
adbClient = null;
}
if (transport) {
await transport.close();
transport = null;
}
connectButton.classList.toggle('hidden');
disconnectButton.classList.toggle('hidden');
startButton.disabled = true;
stopButton.disabled = true;
screenshotButton.disabled = true;
status.textContent = 'Connect to a device to start';
} catch(e) {
console.error('Disconnecting Failed: ', e);
}
});
const RECORD_FILE_NAME = '/data/local/tmp/webadb-record.mp4';
let shell: Stream | null = null;
startButton.addEventListener('click', async() => {
shell = await Stream.open(adbClient!, `shell:screenrecord ${RECORD_FILE_NAME}`, options);
status.textContent = 'Recording...';
stopButton.classList.toggle('hidden');
startButton.classList.toggle('hidden');
});
stopButton.addEventListener('click', async() => {
// await shell!.write(String.fromCharCode(3) + '\n'); // CTRL+C
status.textContent = 'Finishing Recording...';
await shell!.close();
status.textContent = 'Pulling video...';
// Trying to load the file straight away results in a broken file.
// Waiting for a couple of seconds fixes it. Maybe send STAT before
// attempting download.
setTimeout(async () => {
const result = await adbClient!.pull(RECORD_FILE_NAME);
const videoSrc = window.URL.createObjectURL(result);
video!.src = videoSrc;
download!.href = videoSrc;
download.download = 'recording.mp4';
stopButton.classList.toggle('hidden');
startButton.classList.toggle('hidden');
screenshot.classList.add('hidden');
video.classList.remove('hidden');
download.classList.remove('hidden');
status.textContent = 'Done! Connected and ready';
}, 2000);
});
screenshotButton.addEventListener('click', async() => {
status.textContent = 'Generating Screenshot...';
await adbClient!.shell('screencap -p /data/local/tmp/screenshot.png');
status.textContent = 'Pulling image...';
const result = await adbClient!.pull('/data/local/tmp/screenshot.png');
const imageSrc = window.URL.createObjectURL(result);
screenshot.src = imageSrc;
download.href = imageSrc;
download.download = 'screenshot.png';
download.classList.remove('hidden');
screenshot.classList.remove('hidden');
video.classList.add('hidden');
status.textContent = 'Done! Connected and ready';
});
================================================
FILE: demo/tsconfig.json
================================================
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "es6", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["ES2015", "dom"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./public/js", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"skipLibCheck": true,
},
"include": ["**/*.ts"]
}
================================================
FILE: demo/webpack.config.js
================================================
/**
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
module.exports = {
entry: {
screenrecord: './src/screenrecord.ts',
interactiveshell: './src/interactiveshell.ts',
livestream: './src/livestream.ts'
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'js/[name].bundle.js',
path: path.resolve(__dirname, 'public'),
},
devServer: {
static: path.resolve(__dirname, 'public'),
watchFiles: [path.resolve(__dirname, '../dist/**/*')],
},
};
================================================
FILE: demo/workbox-config.js
================================================
module.exports = {
"globDirectory": "public/",
"globPatterns": [
"**/*.{html,svg,js,ts}"
],
"swDest": "public/sw.js"
};
================================================
FILE: eslint.config.mjs
================================================
// @ts-check
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
export default [
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 2017,
sourceType: 'module',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
rules: {
...tsPlugin.configs['eslint-recommended'].overrides[0].rules,
...tsPlugin.configs['recommended'].rules,
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-trailing-spaces': 'error',
'eol-last': 'error',
},
},
];
================================================
FILE: jasmine.json
================================================
{
"spec_dir": "dist/spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}
================================================
FILE: package.json
================================================
{
"name": "wadb",
"version": "0.1.0",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"demo/src/**/*.ts\"",
"lint-fix": "eslint \"src/**/*.ts\" \"demo/src/**/*.ts\" --fix",
"build": "tsc -p .",
"test": "tsc -p . && jasmine --config=jasmine.json",
"dev": "concurrently \"tsc -p . --watch\" \"npm run dev --prefix demo\""
},
"files": [
"dist/lib",
"dist/index.d.ts",
"dist/index.js"
],
"author": "André Cipriani Bandarra",
"license": "Apache-2.0",
"devDependencies": {
"concurrently": "^9.0.0",
"@types/jasmine": "^6.0.0",
"@types/node": "^25.3.0",
"@types/w3c-web-usb": "^1.0.13",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"eslint": "^10.0.2",
"fake-indexeddb": "^6.2.5",
"jasmine": "^6.1.0",
"typescript": "^5.9.3"
}
}
================================================
FILE: src/index.ts
================================================
/**
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './lib/AdbClient';
export * from './lib/AdbConnectionInformation';
export * from './lib/IndexedDbKeyStore';
export * from './lib/KeyStore'
export * from './lib/Options';
export * from './lib/Shell';
export * from './lib/Stream';
export * from './lib/SyncFrame';
export * from './lib/message';
export * from './lib/transport';
================================================
FILE: src/lib/AdbClient.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Transport} from './transport/Transport';
import {Options} from './Options';
import {Message, MessageChannel, MessageListener} from './message';
import {KeyStore} from './KeyStore';
import {privateKeyDump} from './Helpers';
import {AdbConnectionInformation} from './AdbConnectionInformation';
import {Stream} from './Stream';
import {Shell} from './Shell';
import {AsyncBlockingQueue} from './Queues';
import {Framebuffer} from './Framebuffer';
const VERSION = 0x01000000;
const VERSION_NO_CHECKSUM = 0x01000001;
const MAX_PAYLOAD = 256 * 1024;
const MACHINE_BANNER = 'host::\0';
export class AdbClient implements MessageListener {
private messageChannel: MessageChannel;
private messageQueue = new AsyncBlockingQueue();
private openStreams: Set = new Set();
/**
* Creates a new AdbClient
*
* @param {Transport} transport the transport layer.
*/
constructor(
readonly transport: Transport,
readonly options: Options,
readonly keyStore: KeyStore,) {
this.messageChannel = new MessageChannel(transport, options, this);
}
registerStream(stream: Stream): void {
this.openStreams.add(stream);
}
unregisterStream(stream: Stream): void {
this.openStreams.delete(stream);
}
newMessage(msg: Message): void {
// Check if this message matches one of the open streams.
const streams = Array.from(this.openStreams);
for (const stream of streams) {
if (stream.consumeMessage(msg)) {
return;
}
}
this.messageQueue.enqueue(msg);
}
public async awaitMessage(): Promise {
return this.messageQueue.dequeue();
}
async connect(): Promise {
const version = this.options.useChecksum ? VERSION : VERSION_NO_CHECKSUM;
const cnxn = Message.cnxn(version, MAX_PAYLOAD, MACHINE_BANNER, this.options.useChecksum);
await this.sendMessage(cnxn); // Send the Message
// Response to connect must be CNXN or AUTH. Ignore different responses until the right one
// arrives.
let response;
do {
response = await this.awaitMessage();
} while (response.header.cmd !== 'CNXN' && response.header.cmd !== 'AUTH');
// Server connected
if (response.header.cmd === 'CNXN') {
if (!response.data) {
throw new Error('Connection doesn\'t have data');
}
return AdbConnectionInformation.fromDataView(response.data);
}
// Server asked to authenticate
response = await this.doAuth(response);
if (!response.data) {
throw new Error('Connection doesn\'t have data');
}
return AdbConnectionInformation.fromDataView(response.data);
}
async disconnect(): Promise {
this.messageChannel.close();
}
async shell(command: string): Promise {
const stream = await Stream.open(this, `shell:${command}`, this.options);
const okayMessage = Message.newMessage('OKAY', stream.localId, stream.remoteId, this.options.useChecksum);
let result = '';
let message;
do {
message = await stream.read();
if (message.header.cmd === 'WRTE') {
await this.sendMessage(okayMessage);
result += message.dataAsString() || '';
}
} while (message.header.cmd !== 'CLSE');
stream.client.unregisterStream(stream);
return result;
}
async framebuffer(): Promise {
return Framebuffer.create(this, this.options);
}
async interactiveShell(callback?: (result: string) => void): Promise {
const stream = await Stream.open(this, 'shell:', this.options);
return new Shell(stream, callback);
}
async sync(): Promise {
return await Stream.open(this, 'sync:', this.options);
}
async pull(filename: string): Promise {
const syncStream = await this.sync();
const result = await syncStream.pull(filename);
await syncStream.close();
return result;
}
/**
* Pushes a blob of data to the device at the specified remote path.
*
* @param {Blob} blob The data to push.
* @param {string} remotePath The path on the device to write the data to.
* @param {string} mode The mode to set on the file (e.g., "0755").
* @param {number} chunkSize The size of data chunks to send at a time.
*/
async push(blob: Blob, remotePath: string, mode: string, chunkSize: number):
Promise {
const syncStream = await this.sync();
await syncStream.push(blob, remotePath, mode, chunkSize);
await syncStream.close();
}
private async doAuth(authResponse: Message): Promise {
if (authResponse.header.cmd !== 'AUTH') {
throw new Error('Not an AUTH response');
}
if (authResponse.header.arg0 !== 1) {
throw new Error(`
Invalid AUTH parameter. Expected 1 and received ${authResponse.header.arg0}`);
}
if (!authResponse.data) {
throw new Error('AUTH message doens\'t contain data');
}
const token = authResponse.data.buffer as ArrayBuffer;
// Try signing with one of the stored keys
const keys = await this.keyStore.loadKeys();
for (const key of keys) {
const signed = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key.privateKey, token);
const signatureMessage =
Message.authSignature(new DataView(signed), this.options.useChecksum);
await this.sendMessage(signatureMessage);
const signatureResponse = await this.awaitMessage();
if (signatureResponse.header.cmd === 'CNXN') {
return signatureResponse;
}
console.log('Received message ', signatureResponse, 'from phone');
}
// None of they saved Keys is usable. Create new key
const key = await AdbClient.generateKey(this.options.dump, this.options.keySize);
await this.keyStore.saveKey(key);
const exportedKey = new DataView(await crypto.subtle.exportKey('spki', key.publicKey));
const keyMessage = Message.authPublicKey(exportedKey, this.options.useChecksum);
await this.sendMessage(keyMessage);
if (this.options.debug) {
console.log('Waiting for key to be accepted on the device.');
}
const keyResponse = await this.awaitMessage()
if (keyResponse.header.cmd !== 'CNXN') {
console.error('AUTH failed. Phone didn\'t accept key', keyResponse);
throw new Error('AUTH failed. Phone didn\'t accept key');
}
return keyResponse;
}
public async sendMessage(m: Message): Promise {
await this.messageChannel.write(m);
}
static async generateKey(dump: boolean, keySize: number): Promise {
const extractable = dump;
const key = await crypto.subtle.generateKey({
name: 'RSASSA-PKCS1-v1_5',
modulusLength: keySize,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-1' }
}, extractable, [ 'sign', 'verify' ])
if (dump) {
await privateKeyDump(key);
}
return key;
}
}
================================================
FILE: src/lib/AdbConnectionInformation.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const PRODUCT_NAME_KEY = 'ro.product.name';
const PRODUCT_MODEL_KEY = 'ro.product.model';
const PRODUCT_DEVICE_KEY = 'ro.product.device';
const FEATURES_KEY = 'features';
const DEFAULT_PRODUCT_VALUE = '';
export class AdbConnectionInformation {
constructor(
readonly productName: string,
readonly productDevice: string,
readonly productModel: string,
readonly features: string[]
){
}
static fromDataView(input: DataView): AdbConnectionInformation {
const textDecoder = new TextDecoder();
const decodedInput = textDecoder.decode(input);
return AdbConnectionInformation.fromString(decodedInput);
}
/**
* Creates an AdbConnectionInformation from a Connection string
* @param input the string sent as data from a Connection response
*/
static fromString(input: string): AdbConnectionInformation {
const start = input.indexOf('::');
const properties = input.substring(start + 2).split(';');
let productName = DEFAULT_PRODUCT_VALUE;
let productDevice = DEFAULT_PRODUCT_VALUE;
let productModel = DEFAULT_PRODUCT_VALUE;
let features: string[] = [];
for (const property of properties) {
if (property.startsWith(PRODUCT_NAME_KEY)) {
productName = property.substring(PRODUCT_NAME_KEY.length + 1);
continue;
}
if (property.startsWith(PRODUCT_MODEL_KEY)) {
productModel = property.substring(PRODUCT_MODEL_KEY.length + 1);
continue;
}
if (property.startsWith(PRODUCT_DEVICE_KEY)) {
productDevice = property.substring(PRODUCT_DEVICE_KEY.length + 1);
continue;
}
if (property.startsWith(FEATURES_KEY)) {
features = property.substring(FEATURES_KEY.length + 1).split(',');
}
}
return new AdbConnectionInformation(productName, productDevice, productModel, features);
}
}
================================================
FILE: src/lib/Framebuffer.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Stream} from './Stream';
import {AdbClient} from './AdbClient';
import {Options} from './Options';
/**
* framebuffer:
* This service is used to send snapshots of the framebuffer to a client.
* It requires sufficient privileges but works as follow:
*
* After the OKAY, the service sends 16-byte binary structure
* containing the following fields (little-endian format):
*
* depth: uint32_t: framebuffer depth
* size: uint32_t: framebuffer size in bytes
* width: uint32_t: framebuffer width in pixels
* height: uint32_t: framebuffer height in pixels
*
* With the current implementation, depth is always 16, and
* size is always width*height*2
*
* Then, each time the client wants a snapshot, it should send
* one byte through the channel, which will trigger the service
* to send it 'size' bytes of framebuffer data.
*
* If the adbd daemon doesn't have sufficient privileges to open
* the framebuffer device, the connection is simply closed immediately.
*
* Definitions from `system/core/adb/daemon/framebuffer_service.cpp`
*
* struct fbinfo {
* unsigned int version;
* unsigned int bpp;
* unsigned int colorSpace;
* unsigned int size;
* unsigned int width;
* unsigned int height;
* unsigned int red_offset;
* unsigned int red_length;
* unsigned int blue_offset;
* unsigned int blue_length;
* unsigned int green_offset;
* unsigned int green_length;
* unsigned int alpha_offset;
* unsigned int alpha_length;
* }
*/
export class Framebuffer {
// static DDMS_RAWIMAGE_VERSION = 2;
static BYTE_LENGTH = 56;
private constructor(
readonly version: number,
readonly bpp: number,
readonly colorSpace: number,
readonly size: number,
readonly width: number,
readonly height: number,
readonly redOffset: number,
readonly redLength: number,
readonly blueOffset: number,
readonly blueLength: number,
readonly greenOffset: number,
readonly greenLength: number,
readonly alphaOffset: number,
readonly alphaLength: number,
readonly imageData: Uint8ClampedArray) {}
static async create(adbClient: AdbClient, options: Options): Promise {
const stream = await Stream.open(adbClient, 'framebuffer:', options);
let message = await stream.read();
if (message.header.cmd !== 'WRTE') {
await stream.write('CLSE');
throw new Error(`Expected WRTE message but received ${message.header.cmd}`);
}
if (!message.data) {
await stream.write('CLSE');
throw new Error('message doesn\'t contain data');
}
await stream.write('OKAY');
const version = message.data.getUint32(0, true);
const bpp = message.data.getUint32(4, true);
const colorSpace = message.data.getUint32(8, true);
const size = message.data.getUint32(12, true);
const width = message.data.getUint32(16, true);
const height = message.data.getUint32(20, true);
const redOffset = message.data.getUint32(24, true);
const redLength = message.data.getUint32(28, true);
const blueOffset = message.data.getUint32(32, true);
const blueLength = message.data.getUint32(36, true);
const greenOffset = message.data.getUint32(40, true);
const greenLength = message.data.getUint32(44, true);
const alphaOffset = message.data.getUint32(48, true);
const alphaLength = message.data.getUint32(52, true);
const buffer = new Uint8Array(size);
let bytesReceived = 0;
let data = new Uint8Array(message.data.buffer.slice(Framebuffer.BYTE_LENGTH));
buffer.set(data, 0);
bytesReceived = data.length;
while (bytesReceived < size) {
message = await stream.read();
if (message.header.cmd === 'CLSE') {
break;
}
if (!message.data) {
await stream.write('CLSE');
throw new Error('message doesn\'t contain data');
}
data = new Uint8Array(message.data.buffer as ArrayBuffer);
buffer.set(data, bytesReceived);
bytesReceived += data.length;
await stream.write('OKAY');
}
await stream.close();
return new Framebuffer(
version,
bpp,
colorSpace,
size,
width,
height,
redOffset,
redLength,
blueOffset,
blueLength,
greenOffset,
greenLength,
alphaOffset,
alphaLength,
Uint8ClampedArray.from(buffer),
);
}
}
================================================
FILE: src/lib/Helpers.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function paddit(text: string, width: number, padding: string): string {
const padlen = width - text.length;
let padded = '';
for (let i = 0; i < padlen; i++) {
padded += padding;
}
return padded + text;
}
export function toHex8(num: number): string {
return paddit(num.toString(16), 2, '0');
}
export function toHex16(num: number): string {
return paddit(num.toString(16), 4, '0');
}
export function toHex32(num: number): string {
return paddit(num.toString(16), 8, '0');
}
export function hexdump(view: DataView, prefix = ''): void {
const decoder = new TextDecoder();
for (let i = 0; i < view.byteLength; i += 16) {
const max = (view.byteLength - i) > 16 ? 16 : (view.byteLength - i);
let row = prefix + toHex16(i) + ' ';
let j;
for (j = 0; j < max; j++) {
row += ' ' + toHex8(view.getUint8(i + j));
}
for (; j < 16; j++){
row += ' ';
}
row += ' | ' + decoder.decode(new DataView(view.buffer, i, max));
console.log(row);
}
}
export function toB64(buffer: ArrayBuffer): string {
return btoa(new Uint8Array(buffer).reduce((s, b) => s + String.fromCharCode(b), ''));
}
export async function privateKeyDump(key: CryptoKeyPair): Promise {
if (!key.privateKey.extractable) {
console.log('cannot dump the private key, it\'s not extractable');
return;
}
const privkey = await crypto.subtle.exportKey('pkcs8', key.privateKey);
console.log(`-----BEGIN PRIVATE KEY-----\n${toB64(privkey)}\n-----END PRIVATE KEY-----`);
}
export async function publicKeyDump(key: CryptoKeyPair): Promise {
if (!key.publicKey.extractable) {
console.log('cannot dump the public key, it\'s not extractable');
return;
}
const pubKey = await crypto.subtle.exportKey('spki', key.publicKey);
console.log(`-----BEGIN PUBLIC KEY-----\n${toB64(pubKey)}'\n-----END PUBLIC KEY-----`);
}
export function encodeCmd(cmd: string): number {
const encoder = new TextEncoder();
const buffer = encoder.encode(cmd).buffer;
const view = new DataView(buffer);
return view.getUint32(0, true);
}
export function decodeCmd(cmd: number): string {
const decoder = new TextDecoder();
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, cmd, true);
return decoder.decode(buffer);
}
================================================
FILE: src/lib/IndexedDbKeyStore.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {KeyStore} from './KeyStore';
const DB_NAME = 'wadb';
const DB_VERSION = 1;
const STORE_NAME = 'keys';
function openDb(): Promise {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
request.result.createObjectStore(STORE_NAME, {autoIncrement: true});
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
/**
* A KeyStore implementation that persists keys across page reloads using
* IndexedDB. CryptoKey objects are stored directly, which avoids the need to
* export and re-import them.
*/
export class IndexedDbKeyStore implements KeyStore {
async loadKeys(): Promise {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly');
const request = tx.objectStore(STORE_NAME).getAll();
request.onsuccess = () => resolve(request.result as CryptoKeyPair[]);
request.onerror = () => reject(request.error);
tx.oncomplete = () => db.close();
});
}
async saveKey(key: CryptoKeyPair): Promise {
const db = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite');
const request = tx.objectStore(STORE_NAME).add(key);
request.onerror = () => reject(request.error);
tx.oncomplete = () => { db.close(); resolve(); };
});
}
}
================================================
FILE: src/lib/KeyStore.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface KeyStore {
loadKeys(): Promise;
saveKey(key: CryptoKeyPair): Promise;
}
================================================
FILE: src/lib/Log.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default class Log {
debug(message: string): void {
console.log(message);
}
info(message: string): void {
console.info(message);
}
error(message: string): void {
console.error(message);
}
}
================================================
FILE: src/lib/Options.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Options {
debug: boolean;
dump: boolean;
useChecksum: boolean;
keySize: number;
}
================================================
FILE: src/lib/Queues.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
type Resolver = (value: T | PromiseLike) => void;
class QueueEntry {
data: T;
next?: QueueEntry
constructor(data: T) {
this.data = data;
}
}
/**
* A linked queue implementation.
*/
export class Queue {
head?: QueueEntry;
tail?: QueueEntry;
/**
* Adds an item to the queue.
* @param data
*/
enqueue(data: T): void {
const newNode = new QueueEntry(data);
if (this.tail) {
this.tail.next = newNode;
}
this.tail = newNode;
// Queue is empty. Initialise the head.
if (!this.head) {
this.head = this.tail;
}
}
/**
* Removes an item from the queue and returns it.
* @returns {T} the removed item.
* @throws an error if the list is empty.
*/
dequeue(): T {
if (this.isEmpty()) {
throw new Error('Cannot dequeue. Queue is empty');
}
const node = this.head!.data;
this.head = this.head!.next;
return node;
}
/**
* Checks if the Queues is empty
* @returns {boolean} true if the Queue is empty.
*/
isEmpty(): boolean {
return this.head == null;
}
}
/**
* The AsyncBlockingQueue implements a queue with an asynchronous programming model. Items can
* be added to the Queue as usual. When dequeing, a Promise is returned.
*
* The promise will resolve instantly if the Queue is not empty. If the Queue is empty, the Promise
* will be resolved when a new item is added to the queue.
*/
export class AsyncBlockingQueue {
private promiseQueue: Queue> = new Queue>();
private resolverQueue: Queue> = new Queue>();
private add(): void {
const promise = new Promise(resolve => {
this.resolverQueue.enqueue(resolve);
});
this.promiseQueue.enqueue(promise);
}
/**
* Enqueues an item
* @param data
*/
enqueue(data: T): void {
if (this.resolverQueue.isEmpty()) {
this.add();
}
const resolve = this.resolverQueue.dequeue();
resolve(data);
}
/**
* Asynchronously dequeues an item. If the queue is empty, the returned Promise is resolved when
* an item is added. Otherwise, it will return one o the existing items.
* @returns {Promise} that resolves to the data.
*/
async dequeue(): Promise {
if (this.promiseQueue.isEmpty()) {
this.add();
}
return this.promiseQueue.dequeue();
}
hasPendingPromises(): boolean {
return !this.promiseQueue.isEmpty();
}
hasPendingResolvers(): boolean {
return !this.resolverQueue.isEmpty();
}
}
================================================
FILE: src/lib/Shell.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Stream} from './Stream';
import {Message} from './message';
type callbackFunction = (text: string) => void;
export class Shell {
private textDecoder = new TextDecoder();
private textEncoder = new TextEncoder();
private messageListener: ((message: Message) => void)[] = [];
private closed = false;
constructor(readonly stream: Stream, readonly callbackFunction?: callbackFunction) {
this.loopRead();
}
private async loopRead(): Promise {
try {
let message;
do {
message = await this.stream.read();
if (message.header.cmd === 'WRTE') {
this.stream.write('OKAY');
const data = this.textDecoder.decode(message.data!);
if (this.callbackFunction) {
this.callbackFunction(data);
}
}
// Resolve Messages waiting for this event
for (const listener of this.messageListener) {
listener(message);
}
} while (!this.closed)
} catch(e) {
console.error('loopRead crashed', e);
}
this.stream.client.unregisterStream(this.stream);
}
private waitForMessage(cmd: string): Promise {
return new Promise(resolve => {
const callback = (message: Message): void => {
if (message.header.cmd === cmd) {
const pos = this.messageListener.indexOf(callback);
this.messageListener.splice(pos, 1);
resolve(message);
}
};
this.messageListener.push(callback);
});
}
async write(command: string): Promise {
const data = this.textEncoder.encode(command);
await this.stream.write('WRTE', new DataView(data.buffer));
await this.waitForMessage('OKAY');
}
async close(): Promise {
this.closed = true;
await this.write('CLSE');
}
}
================================================
FILE: src/lib/Stream.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AdbClient} from './AdbClient';
import {Message} from './message';
import {Options} from './Options';
import {toHex32} from './Helpers';
import {SyncFrame} from './SyncFrame';
import {AsyncBlockingQueue} from './Queues';
export class Stream {
private static nextId = 1;
private messageQueue = new AsyncBlockingQueue();
constructor(readonly client: AdbClient, readonly service: string, readonly localId: number,
readonly remoteId: number, private options: Options) {
}
async close(): Promise {
await this.write('CLSE');
if (this.options.debug) {
console.log(`Closed stream ${this.service}`);
console.log(` local_id: 0x${toHex32(this.localId)}`);
console.log(` remote_id: 0x${toHex32(this.remoteId)}`);
}
this.client.unregisterStream(this);
}
consumeMessage(msg: Message): boolean {
if (msg.header.arg0 === 0 || msg.header.arg0 !== this.remoteId ||
msg.header.arg1 === 0 || msg.header.arg1 !== this.localId) {
return false;
}
this.messageQueue.enqueue(msg);
return true;
}
async write(cmd: string, data?: DataView): Promise {
const message = this.newMessage(cmd, data);
await this.client.sendMessage(message);
}
async read(): Promise {
return this.messageQueue.dequeue();
}
/**
* Sends a message and waits for a specific response message.
*
* @param {Message} m The message to send.
* @param {string} responseCmd The expected command of the response message.
* @throws {Error} If the response message has a different command.
*/
async sendReceive(m: Message, responseCmd: string): Promise {
await this.client.sendMessage(m);
const response = await this.read();
if (response.header.cmd !== responseCmd) {
throw new Error('WRTE/SEND failed: ' + response);
}
}
/**
*
* Retrieves a file from device to a local file. The remote path is the path to
* the file that will be returned. Just as for the SEND sync request the file
* received is split up into chunks. The sync response id is "DATA" and length is
* the chunk size. After follows chunk size number of bytes. This is repeated
* until the file is transferred. Each chunk will not be larger than 64k.
* When the file is transferred a sync response "DONE" is retrieved where the
* length can be ignored.
*
* @param {string} remotePath path to the file to be pulled from the device
* @returns {Promise} a Blog with the file contents.
*/
async pull(remotePath: string): Promise {
const encoder = new TextEncoder();
const encodedFilename = encoder.encode(remotePath);
// Sends RECV with filename length.
const recvFrame = new SyncFrame('RECV', encodedFilename.byteLength);
const wrteRecvMessage = this.newMessage('WRTE', recvFrame.toDataView());
await this.client.sendMessage(wrteRecvMessage);
const wrteRecvResponse = await this.read();
if (wrteRecvResponse.header.cmd !== 'OKAY') {
throw new Error('WRTE/RECV failed: ' + wrteRecvResponse);
}
// 17. We send the path of the file we want again sdcard/someFile.txt
const wrteFilenameMessage = this.newMessage('WRTE', new DataView(encodedFilename.buffer));
await this.client.sendMessage(wrteFilenameMessage);
// 18. Device sends us OKAY
const wrteFilenameResponse = await this.read();
if (wrteFilenameResponse.header.cmd !== 'OKAY') {
throw new Error('WRTE/filename failed: ' + wrteFilenameResponse);
}
const okayMessage = this.newMessage('OKAY');
let fileDataMessage = await this.read();
while (!fileDataMessage.data) {
fileDataMessage = await this.read();
}
await this.client.sendMessage(okayMessage);
let syncFrame = SyncFrame.fromDataView(new DataView(fileDataMessage.data.buffer.slice(0, 8)));
let buffer = new Uint8Array(fileDataMessage.data.buffer.slice(8));
const chunks: ArrayBuffer[] = [];
while (syncFrame.cmd !== 'DONE') {
while (syncFrame.byteLength >= buffer.byteLength) {
fileDataMessage = await this.read();
if (!fileDataMessage.data) {
continue;
}
await this.client.sendMessage(okayMessage);
// Join both arrays
const newLength = buffer.byteLength + fileDataMessage.data.byteLength;
const newBuffer = new Uint8Array(newLength);
newBuffer.set(buffer, 0);
newBuffer.set(new Uint8Array(fileDataMessage.data.buffer), buffer.byteLength);
buffer = newBuffer;
}
chunks.push(buffer.slice(0, syncFrame.byteLength).buffer);
buffer = buffer.slice(syncFrame.byteLength);
syncFrame = SyncFrame.fromDataView(new DataView(buffer.slice(0, 8).buffer));
buffer = buffer.slice(8);
}
return new Blob(chunks);
}
/**
* Pushes a blob of data to the device at the specified remote path.
*
* @param {Blob} blob The data to push.
* @param {string} remotePath The path on the device to write the data to.
* @param {string} mode The mode to set on the file.
* @param {number} chunkSize The size of data chunks to send at a time.
*/
async push(blob: Blob, remotePath: string, mode: string, chunkSize: number):
Promise {
const reader = new FileReader();
const encoder = new TextEncoder();
// Encodes the remote path for sending over ADB.
const encodedFilename = encoder.encode(remotePath);
// --- Negotiation Phase ---
// 1. Sends SEND command with total filename+mode length.
const sendFrame =
new SyncFrame('SEND', remotePath.length + 1 + mode.length);
const wrteSendMessage = this.newMessage('WRTE', sendFrame.toDataView());
await this.sendReceive(wrteSendMessage, 'OKAY');
// 2. Sends the filename.
const wrteFilenameMessage =
this.newMessage('WRTE', new DataView(encodedFilename.buffer));
await this.sendReceive(wrteFilenameMessage, 'OKAY');
// 3. Sends the mode.
const wrteModeMessage = this.newMessage(
'WRTE', new DataView(encoder.encode(',' + mode).buffer));
await this.sendReceive(wrteModeMessage, 'OKAY');
// --- Data Transfer Phase ---
// 1. Reads the Blob as an ArrayBuffer.
const arrayBufferPromise = new Promise((resolve, reject) => {
reader.onload = (event) => {
return resolve(event.target!.result as ArrayBuffer);
};
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
});
const buffer: ArrayBuffer = await arrayBufferPromise;
// 2. Splits the buffer into chunks.
const chunks: ArrayBufferLike[] = [];
for (let i = 0; i < buffer.byteLength; i += chunkSize) {
chunks.push(buffer.slice(i, Math.min(i + chunkSize, buffer.byteLength)));
}
// 3. Sends each chunk with its size.
for (const chunk of chunks) {
const syncFrame = new SyncFrame('DATA', chunk.byteLength);
const wrteByteLengthMessage =
this.newMessage('WRTE', syncFrame.toDataView());
await this.sendReceive(wrteByteLengthMessage, 'OKAY');
const dataView = new DataView(chunk);
const wrteChunkMessage = this.newMessage('WRTE', dataView);
await this.sendReceive(wrteChunkMessage, 'OKAY');
}
// --- Finishing Up ---
// 1. Sends DONE frame with current timestamp.
const doneFrame = new SyncFrame('DONE', Math.round(Date.now() / 1000));
const doneMessage = this.newMessage('WRTE', doneFrame.toDataView());
await this.client.sendMessage(doneMessage);
// 2. Reads response (should be OKAY) and send final OKAY.
const okayMessage = this.newMessage('OKAY');
await this.sendReceive(okayMessage, 'OKAY');
}
private newMessage(cmd: string, data?: DataView): Message {
return Message.newMessage(
cmd, this.localId, this.remoteId, this.options.useChecksum, data);
}
static async open(adbClient: AdbClient, service: string, options: Options): Promise {
const localId = Stream.nextId++;
let remoteId = 0;
const m = Message.open(localId, remoteId, service, options.useChecksum);
await adbClient.sendMessage(m);
let response;
do {
response = await adbClient.awaitMessage();
} while (response.header.arg1 !== localId);
if (response.header.cmd !== 'OKAY') {
throw new Error('OPEN Failed');
}
remoteId = response.header.arg0;
if (options.debug) {
console.log(`Opened stream ${service}`);
console.log(` local_id: 0x${toHex32(localId)}`);
console.log(` remote_id: 0x${toHex32(remoteId)}`);
}
const stream = new Stream(adbClient, service, localId, remoteId, options);
adbClient.registerStream(stream);
return stream;
}
}
================================================
FILE: src/lib/SyncFrame.ts
================================================
/**
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {encodeCmd, decodeCmd} from './Helpers';
export class SyncFrame {
constructor(readonly cmd: string, readonly byteLength: number) {
}
toDataView(): DataView {
const data = new ArrayBuffer(8);
const cmd = encodeCmd(this.cmd);
const view = new DataView(data);
view.setUint32(0, cmd, true);
view.setUint32(4, this.byteLength, true);
return view;
}
static fromDataView(dataView: DataView): SyncFrame {
const cmd = decodeCmd(dataView.getUint32(0, true));
const byteLength = dataView.getUint32(4, true);
return new SyncFrame(cmd, byteLength);
}
}
================================================
FILE: src/lib/message/Message.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {MessageHeader} from './MessageHeader';
import {toB64} from '../Helpers';
/**
* An ADB Message. Contains a {@link MessageHeader} and an optional {@link DataView} with the
* data for the message.
*/
export class Message {
constructor(
readonly header: MessageHeader,
readonly data?: DataView,
){}
/**
* Returns the data content as a {@link string} or {@link null} if data is not available.
* @returns {string | null} a {@link string} or {@link null} if data is not available.
*/
dataAsString(): string | null {
if (!this.data) {
return null;
}
const textDecoder = new TextDecoder();
return textDecoder.decode(this.data);
}
/**
* Creates a new Message. See {@link MessageHeader}.
* @param {string} cmd the command.
* @param {number} arg0 value for the first argument.
* @param {number} arg1 value for the second argument.
* @param {boolean} useChecksum if the checksum for the data should be calculated.
* @param {DataView} data message data.
* @returns {Message} a new Message
*/
static newMessage(
cmd: string, arg0: number, arg1: number, useChecksum: boolean, data?: DataView): Message {
let checksum = 0;
let byteLength = 0;
if (data) {
byteLength = data.byteLength;
if (useChecksum) {
checksum = Message.checksum(data);
}
}
const header = new MessageHeader(cmd, arg0, arg1, byteLength, checksum);
return new Message(header, data);
}
/**
* Creates a new `OPEN` message.
* @param {number} localId local stream ID
* @param {number} remoteId remote stream ID.
* @param {string} service service description
* @param {boolean} useChecksum if the checksum for the data should be calculated.
* @returns {Message} a correctly setup message with an 'OPEN' command
*/
static open(localId: number, remoteId: number, service: string, useChecksum: boolean): Message {
const encoder = new TextEncoder();
const data = new DataView(encoder.encode('' + service + '\0').buffer);
return Message.newMessage('OPEN', localId, remoteId, useChecksum, data);
}
/**
* Creates a new `CNXN` message.
* @param {number} version version of the protocol to be used.
* @param {number} maxPayload maximum payload size for the connection.
* @param {string} banner host description.
* @param {boolean} useChecksum if the checksum for the data should be calculated.
* @returns {Message} a correctly setup message with an 'CNXN' command
*/
static cnxn(version: number, maxPayload: number, banner: string, useChecksum: boolean): Message {
const encoder = new TextEncoder();
const data = new DataView(encoder.encode(banner).buffer);
return Message.newMessage('CNXN', version, maxPayload, useChecksum, data);
}
/**
* Creates a new `AUTH` message, with the a signed token.
* @param {DataView} signedToken a DataView with the signed token.
* @param {boolean} useChecksum if the checksum for the data should be calculated.
* @returns {Message} a correctly setup message with an 'AUTH' command
*/
static authSignature(signedToken: DataView, useChecksum: boolean): Message {
return Message.newMessage('AUTH', 2, 0, useChecksum, signedToken);
}
/**
* Creates a new `AUTH` message, with the a Public Key.
* @param {DataView} publicKey a DataView with the public key
* @param {boolean} useChecksum if the checksum for the data should be calculated.
* @returns {Message} a correctly setup message with an 'AUTH' command
*/
static authPublicKey(publicKey: DataView, useChecksum: boolean): Message {
const textEncoder = new TextEncoder();
const data = textEncoder.encode(toB64(publicKey.buffer as ArrayBuffer) + '\0');
return Message.newMessage('AUTH', 3, 0, useChecksum, new DataView(data.buffer));
}
private static checksum(dataView: DataView): number {
let sum = 0;
for (let i = 0; i < dataView.byteLength; i++) {
sum += dataView.getUint8(i);
}
return sum & 0xffffffff;
}
}
================================================
FILE: src/lib/message/MessageChannel.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Transport} from '../transport';
import {Message} from './Message';
import {MessageHeader} from './MessageHeader';
import {Options} from '../Options';
import {MessageListener} from './MessageListener';
export class MessageChannel {
private active = true;
constructor(
readonly transport: Transport,
readonly options: Options,
readonly listener: MessageListener) {
this.readLoop();
}
private async readLoop(): Promise {
let message: Message;
do {
message = await this.read();
if (this.options.debug) {
console.log('<<<', message);
}
this.listener.newMessage(message);
} while(this.active);
}
private async readHeader(): Promise {
const response = await this.transport.read(24);
return MessageHeader.parse(response, this.options.useChecksum);
}
private async read(): Promise {
const header = await this.readHeader();
let receivedData;
switch (header.cmd) {
default: {
if (header.length > 0) {
receivedData = await this.transport.read(header.length);
}
}
}
const message = new Message(header, receivedData);
return message;
}
close(): void {
this.active = false;
}
async write(m: Message): Promise {
if (this.options.debug) {
console.log('>>>', m);
}
const data = m.header.toDataView();
await this.transport.write(data.buffer as ArrayBuffer);
if (m.data) {
await this.transport.write(m.data.buffer as ArrayBuffer);
}
}
}
================================================
FILE: src/lib/message/MessageHeader.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {encodeCmd, decodeCmd} from '../Helpers';
/**
* The header of an ADB message. A header is made of 6 fields, each one with 4 bytes:
*
* - command: The command that this message represents.
* - arg0: The meaning depends on the command.
* - arg1: The meaning depends on the command.
* - length: The length of the data part of the message.
* - checksum: Checksum for the data part of the message. Only used in version 0x01000000 of the
* protocol.
* - magic: a checksum for the command. Effectivelly, `command ^ 0xffffffff`.
*/
export class MessageHeader {
/**
* Creates a new MessageHeader
*
* @param {string} cmd The command that this message represents.
* @param {number} arg0 The meaning depends on the command.
* @param {number} arg1 The meaning depends on the command.
* @param {number} length The length of the data part of the message.
* @param {number} checksum Checksum for the data part of the message. Only used in version 0x01000000 of the
* protocol.
*/
constructor(
readonly cmd: string,
readonly arg0: number,
readonly arg1: number,
readonly length: number,
readonly checksum: number) {
}
/**
* Converts the MessageHeader into a {@link DataView}.
* @returns {DataView} a DataView with 24 bytes, with the header content.
*/
toDataView(): DataView {
const view = new DataView(new ArrayBuffer(24));
const rawCmd = encodeCmd(this.cmd);
const magic = rawCmd ^ 0xffffffff;
view.setUint32(0, rawCmd, true);
view.setUint32(4, this.arg0, true);
view.setUint32(8, this.arg1, true);
view.setUint32(12, this.length, true);
view.setUint32(16, this.checksum, true);
view.setUint32(20, magic, true);
return view;
}
/**
* Creates a header from a {@link DataView}.
* @param {DataView} data the {@link DataView} that will be used to create the header.
* @param {boolean} useChecksum if the checksum should be verified.
*/
static parse(data: DataView, useChecksum = false): MessageHeader {
const cmd = data.getUint32(0, true);
const arg0 = data.getUint32(4, true);
const arg1 = data.getUint32(8, true);
const len = data.getUint32(12, true);
const checksum = data.getUint32(16, true);
// Android seems to have stopped providing checksums
if (useChecksum && data.byteLength > 20) {
const magic = data.getUint32(20, true);
if ((cmd ^ magic) !== -1) {
throw new Error('magic mismatch');
}
}
return new MessageHeader(decodeCmd(cmd), arg0, arg1, len, checksum);
}
}
================================================
FILE: src/lib/message/MessageListener.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Message} from './Message';
export interface MessageListener {
newMessage(msg: Message): void;
}
================================================
FILE: src/lib/message/index.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Message';
export * from './MessageChannel';
export * from './MessageHeader';
export * from './MessageListener';
================================================
FILE: src/lib/transport/Transport.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A transport layer for data. Implementations must provide a read and write method.
*/
export interface Transport {
/**
* Writes data to the transport layer.
* @param {DataView} data the data to be written to the layer.
*/
write(data: ArrayBuffer): Promise;
/**
* Reands `len` bytes from the transport layer.
* @param {number} len the number of bytes to read.
*/
read(len: number): Promise;
}
================================================
FILE: src/lib/transport/WebUsbTransport.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Transport} from './Transport';
import {hexdump} from '../Helpers';
import {Options} from '../Options';
const ADB_DEVICE = {classCode: 255, subclassCode: 66, protocolCode: 1} as USBDeviceFilter;
const FASTBOOT_DEVICE = {classCode: 255, subclassCode: 66, protocolCode: 3} as USBDeviceFilter;
const DEVICE_FILTERS = [ADB_DEVICE, FASTBOOT_DEVICE];
interface DeviceMatch {
conf: USBConfiguration;
intf: USBInterface;
alternate: USBAlternateInterface;
}
/**
* An implementation of {@link Transport} using WebUSB as the tranport layer.
*/
export class WebUsbTransport implements Transport {
private constructor(
readonly device: USBDevice,
readonly match: DeviceMatch,
readonly endpointIn: number,
readonly endpointOut: number,
readonly options: Options,
readonly log = console.log) {
}
/**
* Releases the interface and closes the connection to the WebUSB device
*/
async close(): Promise {
await this.device.releaseInterface(this.match.intf.interfaceNumber);
await this.device.close();
}
/**
* Sends data to the USB device
*
* @param {ArrayBuffer} data the data to be sent to the interface
*/
async write(data: ArrayBuffer): Promise {
if (this.options.dump) {
hexdump(new DataView(data), '' + this.endpointOut + '==> ');
}
await this.device.transferOut(this.endpointOut, data);
}
/**
* Receives data from the USB device
*
* @param {number} len the length of date to be read
* @returns {Promise {
const response = await this.device.transferIn(this.endpointIn, len);
if (!response.data) {
throw new Error('Response didn\'t contain any data');
}
return response.data;
}
/**
* @returns {boolean} true if the connected device is an ADB device.
*/
isAdb(): boolean {
const match = WebUsbTransport.findMatch(this.device, ADB_DEVICE);
return match != null;
};
/**
* @returns {boolean} true if the connected device is a Fastboot device.
*/
isFastboot(): boolean {
const match = WebUsbTransport.findMatch(this.device, FASTBOOT_DEVICE);
return match != null;
};
/**
* Opens a connection to a WebUSB device
*
* @param options
*/
static async open(options: Options): Promise {
if (!navigator.usb) {
throw new Error(
'WebUSB is not available. Ensure the page is served over HTTPS or localhost, ' +
'and that you are using a Chromium-based browser.');
}
const device = await navigator.usb.requestDevice({filters: DEVICE_FILTERS});
await device.open();
// Find the WebUSB device
const match = this.findMatch(device, ADB_DEVICE);
if (!match) {
throw new Error('Could not find an ADB device');
}
// Select the configuration and claim the interface
await device.selectConfiguration(match.conf.configurationValue);
await device.claimInterface(match.intf.interfaceNumber);
// await device.selectAlternateInterface(
// match.intf.interfaceNumber, match.alternate.alternateSetting);
// Store the correct endpoints
const endpointIn = WebUsbTransport.getEndpointNum(match.alternate.endpoints, 'in');
const endpointOut = WebUsbTransport.getEndpointNum(match.alternate.endpoints, 'out');
const transport = new WebUsbTransport(device, match, endpointIn, endpointOut, options);
if (options.debug) {
console.log('Created new Transport: ', transport);
}
return transport;
}
private static findMatch(device: USBDevice, filter: USBDeviceFilter): DeviceMatch | null {
for (const configuration of device.configurations) {
for (const intf of configuration.interfaces) {
for (const alternate of intf.alternates) {
if (filter.classCode === alternate.interfaceClass &&
filter.subclassCode === alternate.interfaceSubclass &&
filter.protocolCode === alternate.interfaceProtocol) {
return {
conf: configuration,
intf,
alternate
};
}
}
}
}
return null;
}
private static getEndpointNum(endpoints: USBEndpoint[], dir: 'in' | 'out', type = 'bulk'): number {
for(const ep of endpoints) {
if (ep.direction === dir && ep.type === type) {
return ep.endpointNumber;
}
}
throw new Error(`Cannot find ${dir} endpoint`);
}
}
================================================
FILE: src/lib/transport/index.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Transport';
export * from './WebUsbTransport';
================================================
FILE: src/spec/AdbClientSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AdbClient} from '../lib/AdbClient';
import {MockTransport} from './mock/MockTransport';
import {MockKeyStore} from './mock/MockKeyStore';
import {Options} from '../lib/Options';
describe('AdbClient', () => {
const keyStore = new MockKeyStore();
const options = {
debug: false,
dump: false,
useChecksum: false,
keySize: 2048,
} as Options;
describe('#connect', () => {
let transport: MockTransport;
beforeEach(() => {
transport = new MockTransport();
});
it('Server doesn\'t request AUTH and responds with CNXN', async () => {
await transport.pushFromFile('src/spec/data/messages/connect/connect_simple.json');
const adbClient = new AdbClient(transport, options, keyStore);
const adbDeviceInfo = await adbClient.connect();
expect(adbDeviceInfo).toBeDefined();
});
it('Server responds with AUTH and then CNXN', async () => {
await transport.pushFromFile('src/spec/data/messages/connect/connect_auth_public_key.json');
const adbClient = new AdbClient(transport, options, keyStore);
const adbDeviceInfo = await adbClient.connect();
expect(adbDeviceInfo).toBeDefined();
});
});
});
================================================
FILE: src/spec/IndexedDbKeyStoreSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {IDBFactory} from 'fake-indexeddb';
import {IndexedDbKeyStore} from '../lib/IndexedDbKeyStore';
// A minimal CryptoKeyPair stub for testing storage/retrieval without Web Crypto.
function makeFakeKeyPair(id: number): CryptoKeyPair {
return {
privateKey: {type: 'private', id} as unknown as CryptoKey,
publicKey: {type: 'public', id} as unknown as CryptoKey,
};
}
describe('IndexedDbKeyStore', () => {
beforeEach(() => {
// Each test gets a fresh in-memory IndexedDB to prevent state leakage.
(global as unknown as Record)['indexedDB'] = new IDBFactory();
});
describe('#loadKeys', () => {
it('returns an empty array when no keys have been saved', async () => {
const store = new IndexedDbKeyStore();
const keys = await store.loadKeys();
expect(keys).toEqual([]);
});
});
describe('#saveKey', () => {
it('persists a key that is returned by loadKeys', async () => {
const store = new IndexedDbKeyStore();
const pair = makeFakeKeyPair(1);
await store.saveKey(pair);
const keys = await store.loadKeys();
expect(keys.length).toBe(1);
expect(keys[0]).toEqual(pair);
});
it('persists multiple keys in insertion order', async () => {
const store = new IndexedDbKeyStore();
const pair1 = makeFakeKeyPair(1);
const pair2 = makeFakeKeyPair(2);
await store.saveKey(pair1);
await store.saveKey(pair2);
const keys = await store.loadKeys();
expect(keys.length).toBe(2);
expect(keys[0]).toEqual(pair1);
expect(keys[1]).toEqual(pair2);
});
it('keys survive across separate IndexedDbKeyStore instances sharing the same DB', async () => {
const store1 = new IndexedDbKeyStore();
const pair = makeFakeKeyPair(1);
await store1.saveKey(pair);
// Simulate a page reload: new instance, same underlying DB (global.indexedDB unchanged).
const store2 = new IndexedDbKeyStore();
const keys = await store2.loadKeys();
expect(keys.length).toBe(1);
expect(keys[0]).toEqual(pair);
});
});
});
================================================
FILE: src/spec/QueuesSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Queue, AsyncBlockingQueue} from '../lib/Queues';
describe('Queues', () => {
describe('Queue', () => {
describe('#constructor', () => {
it('Builds an empty queue', () => {
const queue = new Queue();
expect(queue.isEmpty()).toBeTrue();
});
});
describe('#enqueue', () => {
it('Queue is not empty after enqueing an item', () => {
const queue = new Queue();
queue.enqueue('test');
expect(queue.isEmpty()).toBeFalse();
});
});
describe('#dequeue', () => {
it('Throws Error when dequeing an empty list', () => {
const queue = new Queue();
expect(queue.dequeue).toThrowError();
});
it('Dequeues with one value', () => {
const queue = new Queue();
queue.enqueue('one');
expect(queue.dequeue()).toBe('one');
expect(queue.isEmpty()).toBeTrue();
});
it('Dequeues in the correct order', () => {
const queue = new Queue();
queue.enqueue('one');
queue.enqueue('two');
expect(queue.dequeue()).toBe('one');
expect(queue.dequeue()).toBe('two');
queue.enqueue('three');
expect(queue.dequeue()).toBe('three');
expect(queue.isEmpty()).toBeTrue();
});
});
});
describe('AsyncBlockingQueue', () => {
describe('#contructor', () => {
it('Constructs and empty queue', () => {
const queue = new AsyncBlockingQueue();
expect(queue.hasPendingPromises()).toBeFalse();
expect(queue.hasPendingResolvers()).toBeFalse();
});
});
describe('#enqueue', () => {
it('enqueue adds a pending Promise', () => {
const queue = new AsyncBlockingQueue();
queue.enqueue('test');
expect(queue.hasPendingPromises()).toBeTrue();
expect(queue.hasPendingResolvers()).toBeFalse();
});
it('enqueue after dequeue clears Promises and Resolvers', () => {
const queue = new AsyncBlockingQueue();
queue.dequeue();
queue.enqueue('test');
expect(queue.hasPendingPromises()).toBeFalse();
expect(queue.hasPendingResolvers()).toBeFalse();
});
});
describe('#dequeue', () => {
it('dequeue adds a pending Resolver', () => {
const queue = new AsyncBlockingQueue();
queue.dequeue();
expect(queue.hasPendingPromises()).toBeFalse();
expect(queue.hasPendingResolvers()).toBeTrue();
});
it('dequeue after enqueue clears Promises and Resolvers', () => {
const queue = new AsyncBlockingQueue();
queue.enqueue('test');
queue.dequeue();
expect(queue.hasPendingPromises()).toBeFalse();
expect(queue.hasPendingResolvers()).toBeFalse();
});
it('Dequeues correctly after enqueue', async () => {
const queue = new AsyncBlockingQueue();
queue.enqueue('test');
const value = await queue.dequeue();
expect(value).toBe('test');
});
it('Dequeues correctly when dequeue is called before enqueue', async () => {
const queue = new AsyncBlockingQueue();
const promise = queue.dequeue();
queue.enqueue('test');
const value = await promise;
expect(value).toBe('test');
});
it('Dequeues in the correct order, with enqueues then dequeues', async () => {
const queue = new AsyncBlockingQueue();
queue.enqueue('one');
queue.enqueue('two');
expect(await queue.dequeue()).toBe('one');
expect(await queue.dequeue()).toBe('two');
});
it('Dequeues in the correct order, enqueue > dequeue > dequeue > enqueue', async () => {
const queue = new AsyncBlockingQueue();
queue.enqueue('one');
expect(await queue.dequeue()).toBe('one');
const promise = queue.dequeue();
queue.enqueue('two');
expect(await promise).toBe('two');
});
it('Dequeues in the correct order, dequeue > dequeue > enqueue > enqueue', async () => {
const queue = new AsyncBlockingQueue();
const p1 = queue.dequeue();
const p2 = queue.dequeue();
queue.enqueue('one');
queue.enqueue('two');
expect(await p1).toBe('one');
expect(await p2).toBe('two');
});
});
});
});
================================================
FILE: src/spec/StreamSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AdbClient} from '../lib/AdbClient';
import {MockKeyStore} from './mock/MockKeyStore';
import {Options} from '../lib/Options';
import {MockTransport} from './mock/MockTransport';
import {Stream} from '../lib/Stream';
const options = {
debug: false,
dump: false,
useChecksum: false,
keySize: 2048,
} as Options;
describe('Stream', () => {
describe('#open', () => {
it('Opens a stream', async () => {
const mockTransport = new MockTransport();
await mockTransport.pushFromFile('src/spec/data/messages/stream/open.json');
const adbClient = new AdbClient(mockTransport, options, new MockKeyStore());
const stream = await Stream.open(adbClient, 'test:', options);
expect(stream.localId).toBe(1);
expect(stream.remoteId).toBe(34); // Defined in open.json
expect(stream.service).toBe('test:');
});
});
});
================================================
FILE: src/spec/SyncFrameSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {encodeCmd} from '../lib/Helpers';
import {SyncFrame} from '../lib/SyncFrame';
describe('SyncFrame', () => {
describe('#fromDataView', () => {
it('Reads a SyncFrame from a DataView', () => {
const dataView = new DataView(new ArrayBuffer(8));
dataView.setUint32(0, encodeCmd('WRTE'), true);
dataView.setUint32(4, 256, true);
const syncFrame = SyncFrame.fromDataView(dataView);
expect(syncFrame.cmd).toBe('WRTE');
expect(syncFrame.byteLength).toBe(256);
});
});
describe('#toDataView', () => {
it('Writes a SyncFrame to a DataView', () => {
const syncFrame = new SyncFrame('WRTE', 256);
const dataView = syncFrame.toDataView();
const encodedCmd = encodeCmd('WRTE');
expect(dataView.getUint32(0, true)).toBe(encodedCmd);
expect(dataView.getUint32(4, true)).toBe(256);
});
})
});
================================================
FILE: src/spec/data/messages/connect/connect_auth_public_key.json
================================================
[
{"cmd": "AUTH", "arg0": 1, "arg1": 0, "data": "auth_token"},
{"cmd": "CNXN", "arg0": 1, "arg1": 65536, "data": "host://"}
]
================================================
FILE: src/spec/data/messages/connect/connect_simple.json
================================================
[{"cmd": "CNXN", "arg0": 1, "arg1": 65536, "data": "host://"}]
================================================
FILE: src/spec/data/messages/stream/open.json
================================================
[
{"cmd": "OKAY", "arg0": 34, "arg1": 1}
]
================================================
FILE: src/spec/message/MessageChannelSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {MockTransport} from '../mock/MockTransport';
import {MockMessageListener} from '../mock/MockMessageListener';
import {Message, MessageChannel, MessageHeader} from '../../lib/message';
import {Options} from '../../lib/Options';
describe('MessageChannel', () => {
const options = {
debug: false,
dump: false,
useChecksum: false,
keySize: 2048,
} as Options;
let messageListener: MockMessageListener;
let transport: MockTransport;
let messageChannel: MessageChannel;
describe('#write', () => {
beforeEach(() => {
messageListener = new MockMessageListener();
transport = new MockTransport();
messageChannel = new MessageChannel(transport, options, messageListener);
});
it('writes a message with without data', async () => {
const message = Message.newMessage('CNXN', 1, 2, true);
await messageChannel.write(message);
messageChannel.close();
expect(transport.receivedData.length).toBe(1);
expect(MessageHeader.parse(transport.receivedData[0])).toEqual(message.header);
});
it('writes a message with with data', async () => {
const data = new DataView(new TextEncoder().encode('test').buffer);
const message = Message.newMessage('CNXN', 1, 2, true, data);
await messageChannel.write(message);
messageChannel.close();
expect(transport.receivedData.length).toBe(2);
expect(MessageHeader.parse(transport.receivedData[0])).toEqual(message.header);
expect(transport.receivedData[1].byteLength).toBe(4);
});
});
describe('#readLoop', () => {
const messageWithoutData = Message.newMessage('MOCK', 0, 0, true);
const data = new DataView(new TextEncoder().encode('test').buffer);
const messageWithData = Message.newMessage('MOCK', 0, 0, true, data);
beforeEach(() => {
messageListener = new MockMessageListener();
transport = new MockTransport();
});
it('Receives a Message', async () => {
transport.pushData(messageWithoutData.header.toDataView());
messageChannel = new MessageChannel(transport, options, messageListener);
const receivedMessage = await messageListener.messageQueue.dequeue();
expect(receivedMessage.header).toEqual(messageWithoutData.header);
});
it('Receives a Message with data', async () => {
transport.pushData(messageWithData.header.toDataView());
transport.pushData(messageWithData.data!);
messageChannel = new MessageChannel(transport, options, messageListener);
const receivedMessage = await messageListener.messageQueue.dequeue();
expect(receivedMessage).toEqual(messageWithData);
});
it('Receives Messages in the right order', async () => {
transport.pushData(messageWithoutData.header.toDataView());
transport.pushData(messageWithData.header.toDataView());
transport.pushData(messageWithData.data!);
messageChannel = new MessageChannel(transport, options, messageListener);
const receivedMessage1 = await messageListener.messageQueue.dequeue();
const receivedMessage2 = await messageListener.messageQueue.dequeue();
expect(receivedMessage1).toEqual(messageWithoutData);
expect(receivedMessage2).toEqual(messageWithData);
});
});
});
================================================
FILE: src/spec/message/MessageHeaderSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {MessageHeader} from '../../lib/message';
describe('MessageHeader', () => {
describe('#parse', () => {
it('parses a messsage header, with checksum enabled', () => {
const data = new DataView(new ArrayBuffer(24));
data.setUint32(0, 0x4e584e43, true); // CNXN
data.setUint32(4, 0x01000000, true); // Version
data.setUint32(8, 256 * 1024, true); // Length
data.setUint32(12, 16, true); // Length
data.setUint32(16, 0, true); // Checksum
data.setUint32(20, 0x4e584e43 ^ 0xffffffff, true); // Magic
const header = MessageHeader.parse(data, true);
expect(header.cmd).toBe('CNXN');
expect(header.arg0).toBe(0x01000000);
expect(header.arg1).toBe(256 * 1024);
expect(header.length).toBe(16);
expect(header.checksum).toBe(0);
});
it('Fails to parse a header with an invalid magic / checksum enabled', () => {
const data = new DataView(new ArrayBuffer(24));
data.setUint32(0, 0x4e584e43, true); // CNXN
data.setUint32(4, 0x01000000, true); // Version
data.setUint32(8, 256 * 1024, true); // Length
data.setUint32(12, 16, true); // Length
data.setUint32(16, 0, true); // Checksum
data.setUint32(20, 0, true); // Magic
expect((() => MessageHeader.parse(data, true))).toThrowError('magic mismatch');
});
});
describe('#toDataView', () => {
it ('converts a header to a DataView', () => {
const data = new MessageHeader('CNXN', 0x01000000, 256 * 1024, 16, 0).toDataView();
expect(data.getUint32(0, true)).toBe(0x4e584e43);
expect(data.getUint32(4, true)).toBe(0x01000000);
expect(data.getUint32(8, true)).toBe(256 * 1024);
expect(data.getUint32(12, true)).toBe(16);
expect(data.getUint32(16, true)).toBe(0);
expect(data.getInt32(20, true)).toBe((0x4e584e43 ^ 0xffffffff));
});
});
});
================================================
FILE: src/spec/message/MessageSpec.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Message} from '../../lib/message';
describe('Message', () => {
describe('#newMessage', () => {
it('Creates a message without data', () => {
const message = Message.newMessage('CNXN', 1, 2, true);
expect(message.header.cmd).toBe('CNXN');
expect(message.header.arg0).toBe(1);
expect(message.header.arg1).toBe(2);
expect(message.header.length).toBe(0);
expect(message.header.checksum).toBe(0);
expect(message.data).toBeUndefined();
});
it('Creates a message with data and checksum enabled', () => {
const data = new DataView(new TextEncoder().encode('test').buffer);
const message = Message.newMessage('CNXN', 1, 2, true, data);
expect(message.header.cmd).toBe('CNXN');
expect(message.header.arg0).toBe(1);
expect(message.header.arg1).toBe(2);
expect(message.header.length).toBe(data.byteLength);
expect(message.header.checksum).toBe(448);
expect(message.data).toEqual(data);
});
it('Creates a message with data and checksum disabled', () => {
const data = new DataView(new TextEncoder().encode('test').buffer);
const message = Message.newMessage('CNXN', 1, 2, false, data);
expect(message.header.cmd).toBe('CNXN');
expect(message.header.arg0).toBe(1);
expect(message.header.arg1).toBe(2);
expect(message.header.length).toBe(data.byteLength);
expect(message.header.checksum).toBe(0);
expect(message.data).toEqual(data);
});
});
describe('#dataAsString', () => {
it('Returns correct string value for data', () => {
const data = new DataView(new TextEncoder().encode('test').buffer);
const message = Message.newMessage('CNXN', 1, 2, false, data);
expect(message.dataAsString()).toBe('test');
});
it('Returns null if data is not available', () => {
const message = Message.newMessage('CNXN', 1, 2, false);
expect(message.dataAsString()).toBeNull()
});
});
describe('#open', () => {
it('Creates an OPEN message', () => {
const message = Message.open(1, 2, 'service', true);
expect(message.header.cmd).toBe('OPEN');
expect(message.header.arg0).toBe(1);
expect(message.header.arg1).toBe(2);
expect(message.header.length).toBe(8);
expect(message.header.checksum).toBe(753);
});
});
describe('#cnxn', () => {
it('Creates an CNXN message', () => {
const message = Message.cnxn(1, 2, 'banner', true);
expect(message.header.cmd).toBe('CNXN');
expect(message.header.arg0).toBe(1);
expect(message.header.arg1).toBe(2);
expect(message.header.length).toBe(6);
expect(message.header.checksum).toBe(630);
});
});
describe('#authSignature', () => {
it('Creates an AUTH message with a signed token', () => {
const data = new DataView(new TextEncoder().encode('signed').buffer);
const message = Message.authSignature(data, true);
expect(message.header.cmd).toBe('AUTH');
expect(message.header.arg0).toBe(2);
expect(message.header.arg1).toBe(0);
expect(message.header.length).toBe(6);
expect(message.header.checksum).toBe(634);
});
});
describe('#authPublicKey', () => {
it('Creates an AUTH message with a public key', () => {
// Node doesn't have a global btoa function. We patch it here for the test.
globalThis.btoa = (input: string): string => {
return Buffer.from(input).toString('base64');
};
const data = new DataView(new TextEncoder().encode('publickey').buffer);
const message = Message.authPublicKey(data, true);
expect(message.header.cmd).toBe('AUTH');
expect(message.header.arg0).toBe(3);
expect(message.header.arg1).toBe(0);
expect(message.header.length).toBe(13);
expect(message.header.checksum).toBe(1031);
});
});
});
================================================
FILE: src/spec/mock/MockKeyStore.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {KeyStore} from '../../lib/KeyStore';
export class MockKeyStore implements KeyStore {
loadKeys(): Promise {
return Promise.resolve([]);
}
saveKey(): Promise {
return Promise.resolve();
};
}
================================================
FILE: src/spec/mock/MockMessageListener.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {AsyncBlockingQueue} from '../../lib/Queues';
import {Message, MessageListener} from '../../lib/message';
export class MockMessageListener implements MessageListener {
messageQueue = new AsyncBlockingQueue();
newMessage(msg: Message): void {
this.messageQueue.enqueue(msg);
}
}
================================================
FILE: src/spec/mock/MockTransport.ts
================================================
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Transport} from '../../lib/transport';
import {Message} from '../../lib/message';
import * as fs from 'fs';
export class MockTransport implements Transport {
receivedData: DataView[] = [];
pendingData: ArrayBuffer = new ArrayBuffer(0);
pos = 0;
reject?: (reason: Error) => void;
async pushFromFile(fileName: string): Promise {
const textEncoder = new TextEncoder();
const messages = JSON.parse(await fs.promises.readFile(fileName, {encoding: "utf-8"}));
for (const jsonMessage of messages) {
const cmd = jsonMessage.cmd;
const arg0 = jsonMessage.arg0;
const arg1 = jsonMessage.arg1;
const data = jsonMessage.data ?
new DataView(textEncoder.encode(jsonMessage.data).buffer) : jsonMessage.data;
const useChecksum = !!jsonMessage.useChecksum;
this.pushMessage(Message.newMessage(cmd, arg0, arg1, useChecksum, data));
}
}
pushData(data: DataView): void {
const buffer = data.buffer;
const tmp = new Uint8Array(this.pendingData.byteLength + buffer.byteLength);
tmp.set(new Uint8Array(this.pendingData), 0);
tmp.set(new Uint8Array(buffer), this.pendingData.byteLength);
this.pendingData = tmp.buffer;
}
pushMessage(msg: Message): void {
this.pushData(msg.header.toDataView());
if (msg.data) {
this.pushData(msg.data);
}
}
async read(len: number): Promise {
// Our buffer doesn't have enough data.
if (this.pendingData.byteLength - this.pos < len) {
return new Promise((_, reject) => {
this.reject = reject;
});
}
const dataView = new DataView(this.pendingData, this.pos, len);
this.pos += len;
return dataView;
}
async write(data: ArrayBuffer): Promise {
this.receivedData.push(new DataView(data));
return Promise.resolve();
}
close(): void {
if (this.reject) {
this.reject(new Error('Transport Closed'));
}
}
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"moduleResolution": "node",
"lib": ["ES2015", "DOM"]
},
"typedocOptions": {
"mode": "modules",
"out": "docs",
},
"include": ["src/**/*.ts"]
}