Refactor
This commit is contained in:
parent
43bfb6715c
commit
904b11b7b7
@ -25,8 +25,11 @@ pnpm run bundle
|
|||||||
## Test
|
## Test
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
cd packages/opvault.js/src/__tests__
|
||||||
wget -qO- https://cache.agilebits.com/security-kb/freddy-2013-12-04.tar.gz | tar xvz
|
wget -qO- https://cache.agilebits.com/security-kb/freddy-2013-12-04.tar.gz | tar xvz
|
||||||
mv onepassword_data freddy-2013-12-04.opvault
|
mv onepassword_data freddy-2013-12-04.opvault
|
||||||
|
|
||||||
|
# Run tests
|
||||||
pnpm run test
|
pnpm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
55
package.json
55
package.json
@ -6,46 +6,47 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"design": "marked -o design.html < design.md",
|
"design": "marked -o design.html < design.md",
|
||||||
"test": "node --expose-gc node_modules/mocha/bin/_mocha test/**/*.test.ts",
|
"test": "rm -rf mochawesome-report; c8 -r html node --expose-gc node_modules/mocha/bin/_mocha packages/**/*.test.ts; mv coverage mochawesome-report/coverage",
|
||||||
"repl": "node -r ts-node/register/transpile-only src/repl.ts",
|
"repl": "node -r ts-node/register/transpile-only src/repl.ts",
|
||||||
"dev": "cd packages/web && yarn dev",
|
"dev": "cd packages/web && yarn dev",
|
||||||
"bundle": "cd packages/web && yarn bundle",
|
"bundle": "cd packages/web && yarn bundle",
|
||||||
"i18n": "node packages/web/scripts/build-i18n-yml-typedef.js"
|
"i18n": "node packages/web/scripts/build-i18n-yml-typedef.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.4",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
|
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
|
||||||
"@types/node": "^17.0.23",
|
"@types/node": "^18.16.2",
|
||||||
"@types/sinon": "^10.0.11",
|
"@types/sinon": "^10.0.14",
|
||||||
"@types/sinon-chai": "^3.2.8",
|
"@types/sinon-chai": "^3.2.9",
|
||||||
"@types/wicg-file-system-access": "^2020.9.5",
|
"@types/wicg-file-system-access": "^2020.9.5",
|
||||||
"@typescript-eslint/eslint-plugin": "5.17.0",
|
"@typescript-eslint/eslint-plugin": "5.59.1",
|
||||||
"@typescript-eslint/parser": "5.17.0",
|
"@typescript-eslint/parser": "5.59.1",
|
||||||
"chai": "^4.3.6",
|
"c8": "^7.13.0",
|
||||||
|
"chai": "^4.3.7",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"eslint": "8.12.0",
|
"eslint": "8.39.0",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-import-resolver-typescript": "2.7.0",
|
"eslint-import-resolver-typescript": "3.5.5",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-react": "7.29.4",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"eslint-plugin-react-hooks": "4.4.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^11.1.1",
|
||||||
"marked": "^4.0.12",
|
"marked": "^4.3.0",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^10.2.0",
|
||||||
"mochawesome": "^7.1.3",
|
"mochawesome": "^7.1.3",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.8.8",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"sass": "^1.49.11",
|
"sass": "^1.62.1",
|
||||||
"sinon": "^13.0.1",
|
"sinon": "^15.0.4",
|
||||||
"sinon-chai": "^3.7.0",
|
"sinon-chai": "^3.7.0",
|
||||||
"tslib": "^2.3.1",
|
"ts-node": "^10.9.1",
|
||||||
"ts-node": "^10.7.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"tsconfig-paths": "^3.14.1",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^4.6.3"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "opvault-adapters",
|
|
||||||
"dependencies": {
|
|
||||||
"opvault.js": "*"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "opvault.js",
|
"name": "opvault.js",
|
||||||
"main": "src/index.ts",
|
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c; prettier --write lib >/dev/null",
|
"build": "rollup -c --bundleConfigAsCjs; prettier --write lib >/dev/null",
|
||||||
"build:docs": "typedoc --out docs src/index.ts --excludePrivate"
|
"build:docs": "typedoc --out docs src/index.ts --excludePrivate"
|
||||||
},
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./lib/index.js",
|
||||||
|
"./node": "./lib/node.js",
|
||||||
|
"./filePicker": "./lib/filePicker.js",
|
||||||
|
"./webkit": "./lib/webkit.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"tiny-invariant": "1.2.0",
|
"tiny-invariant": "1.3.1",
|
||||||
"tslib": "2.3.1"
|
"tslib": "2.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
"@rollup/plugin-replace": "^3.0.0",
|
"@rollup/plugin-replace": "^5.0.2",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.8.8",
|
||||||
"rollup": "^2.61.1",
|
"rollup": "^3.21.0",
|
||||||
"rollup-plugin-ts": "^2.0.4",
|
"rollup-plugin-ts": "^3.2.0",
|
||||||
"typedoc": "^0.22.10"
|
"typedoc": "^0.24.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,9 @@ import { dependencies } from "./package.json"
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
input: {
|
input: {
|
||||||
index: "./src/index.ts",
|
index: "./src/index.ts",
|
||||||
|
node: "./src/adapter/node.ts",
|
||||||
|
filePicker: "./src/adapter/showDirectoryPicker.ts",
|
||||||
|
webkit: "./src/adapter/webkitdirectory.ts",
|
||||||
},
|
},
|
||||||
external: builtinModules.concat(Object.keys(dependencies)),
|
external: builtinModules.concat(Object.keys(dependencies)),
|
||||||
output: {
|
output: {
|
||||||
@ -21,8 +24,6 @@ export default () => ({
|
|||||||
preventAssignment: true,
|
preventAssignment: true,
|
||||||
values: {
|
values: {
|
||||||
"process.env.NODE_ENV": '"production"',
|
"process.env.NODE_ENV": '"production"',
|
||||||
'require("./adapter").nodeAdapter':
|
|
||||||
'import("./adapter").then(x => x.nodeAdapter)',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
99
packages/opvault.js/src/__tests__/index.test.ts
Normal file
99
packages/opvault.js/src/__tests__/index.test.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { resolve } from "path"
|
||||||
|
import { describe, it, beforeEach } from "mocha"
|
||||||
|
import { expect } from "chai"
|
||||||
|
|
||||||
|
import type { Vault } from "../index"
|
||||||
|
import { base64FromByteArray } from "../buffer"
|
||||||
|
import { OnePassword } from "../index"
|
||||||
|
import { adapter } from "../adapter/node"
|
||||||
|
|
||||||
|
describe("OnePassword", () => {
|
||||||
|
const freddy = resolve(__dirname, "freddy-2013-12-04.opvault")
|
||||||
|
|
||||||
|
describe("getProfileNames", () => {
|
||||||
|
it("freddy", async () => {
|
||||||
|
const instance = new OnePassword({ path: freddy, adapter })
|
||||||
|
expect(await instance.getProfileNames()).to.deep.equal(["default"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip("ignores faulty folders", async () => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("unlock", () => {
|
||||||
|
let vault: Vault
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vault = await new OnePassword({ path: freddy, adapter }).getProfile("default")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("accepts correct password", async () => {
|
||||||
|
await expect(vault.unlock("freddy")).to.be.fulfilled
|
||||||
|
expect(vault.isLocked).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects wrong password", () => {
|
||||||
|
;["Freddy", "_freddy", ""].forEach(async password => {
|
||||||
|
await expect(vault.unlock(password)).to.be.rejectedWith("Invalid password")
|
||||||
|
expect(vault.isLocked).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("content", () => {
|
||||||
|
let vault: Vault
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vault = await new OnePassword({ path: freddy, adapter }).getProfile("default")
|
||||||
|
await vault.unlock("freddy")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("reads notes", async () => {
|
||||||
|
const item = (await vault.getItem({
|
||||||
|
title: "A note with some attachments",
|
||||||
|
}))!
|
||||||
|
expect(item).to.exist
|
||||||
|
expect(item.uuid).to.equal("F2DB5DA3FCA64372A751E0E85C67A538")
|
||||||
|
expect(item.attachments).to.have.lengthOf(2)
|
||||||
|
expect(item.details).to.deep.equal({
|
||||||
|
notesPlain: "This note has two attachments.",
|
||||||
|
})
|
||||||
|
expect(item.overview).to.deep.equal({
|
||||||
|
title: "A note with some attachments",
|
||||||
|
ps: 0,
|
||||||
|
ainfo: "This note has two attachments.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("decrypts items", async () => {
|
||||||
|
const decrypted = require("./decrypted.json")
|
||||||
|
expect(vault.isLocked).to.be.false
|
||||||
|
for (const [uuid, item] of Object.entries<any>(decrypted)) {
|
||||||
|
const actual = await vault.getItem(uuid)
|
||||||
|
expect(actual).to.exist
|
||||||
|
expect(actual!.overview).to.deep.equal(item.overview)
|
||||||
|
expect(actual!.details).to.deep.equal(item.itemDetails)
|
||||||
|
expect(actual!.attachments).to.have.lengthOf(item.attachments.length)
|
||||||
|
for (const [i, attachment] of actual!.attachments.entries()) {
|
||||||
|
const expected = item.attachments[i]
|
||||||
|
await attachment.unlock()
|
||||||
|
expect(attachment.metadata).to.deep.equal(expected.metadata)
|
||||||
|
expect(base64FromByteArray(attachment.file)).to.deep.equal(expected.file)
|
||||||
|
expect(base64FromByteArray(attachment.icon)).to.deep.equal(expected.icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("lock", () => {
|
||||||
|
it("locks", async () => {
|
||||||
|
const instance = new OnePassword({ path: freddy, adapter })
|
||||||
|
const vault = await instance.getProfile("default")
|
||||||
|
await vault.unlock("freddy")
|
||||||
|
expect(vault.isLocked).to.be.false
|
||||||
|
|
||||||
|
vault.lock()
|
||||||
|
expect(vault.isLocked).to.be.true
|
||||||
|
expect(vault.getItem("F2DB5DA3FCA64372A751E0E85C67A538")).to.eventually.throw
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
43
packages/opvault.js/src/__tests__/weakMap.test.ts
Normal file
43
packages/opvault.js/src/__tests__/weakMap.test.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it } from "mocha"
|
||||||
|
import { expect } from "chai"
|
||||||
|
|
||||||
|
import { WeakValueMap } from "../weakMap"
|
||||||
|
|
||||||
|
declare const gc: () => void
|
||||||
|
|
||||||
|
describe("WeakValueMap", () => {
|
||||||
|
interface Value {
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
it("covers base use cases", () => {
|
||||||
|
const map = new WeakValueMap<string, Value>()
|
||||||
|
const object = { value: 1 }
|
||||||
|
map.set("key", object)
|
||||||
|
expect(map.get("key")!.value).to.equal(1)
|
||||||
|
expect(map.delete("key")).to.be.true
|
||||||
|
expect(!map.delete("key")).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it("overrides previous value", () => {
|
||||||
|
const map = new WeakValueMap<string, Value>()
|
||||||
|
map.set("key", { value: 2 })
|
||||||
|
map.set("key", { value: 3 })
|
||||||
|
expect(map.get("key")!.value).to.equal(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes garbage collected values", done => {
|
||||||
|
const map = new WeakValueMap<string, Value>()
|
||||||
|
map.set("key", { value: 1 })
|
||||||
|
setTimeout(() => {
|
||||||
|
gc()
|
||||||
|
expect(map.has("key")).to.be.false
|
||||||
|
map.set("key", { value: 2 })
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
gc()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,10 +1,7 @@
|
|||||||
import { promises as fs, existsSync } from "fs"
|
|
||||||
import { webcrypto } from "crypto"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object that implements basic file system functionalities.
|
* An object that implements basic file system functionalities.
|
||||||
*/
|
*/
|
||||||
export interface IFileSystem {
|
export interface FileSystem {
|
||||||
/**
|
/**
|
||||||
* Asynchronously tests whether or not the given path exists by checking with the file system.
|
* Asynchronously tests whether or not the given path exists by checking with the file system.
|
||||||
* @param path A path to a file or directory.
|
* @param path A path to a file or directory.
|
||||||
@ -15,39 +12,45 @@ export interface IFileSystem {
|
|||||||
* Asynchronously reads the entire contents of a file.
|
* Asynchronously reads the entire contents of a file.
|
||||||
* @param path A path to a file.
|
* @param path A path to a file.
|
||||||
*/
|
*/
|
||||||
readBuffer(path: string): Promise<Buffer>
|
readFile(path: string): Promise<Uint8Array>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously reads the entire contents of a file.
|
* Asynchronously reads the entire contents of a file.
|
||||||
* @param path A path to a file.
|
* @param path A path to a file.
|
||||||
*/
|
*/
|
||||||
readFile(path: string): Promise<string>
|
readTextFile(path: string): Promise<string>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously writes data to a file, replacing the file if it already exists.
|
* Asynchronously writes data to a file, replacing the file if it already exists.
|
||||||
* @param path A path to a file.
|
* @param path A path to a file.
|
||||||
* @param data The data to write.
|
* @param data The data to write.
|
||||||
*/
|
*/
|
||||||
writeFile(path: string, data: string): Promise<void>
|
writeTextFile(path: string, data: string): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronous readdir(3) - read a directory.
|
* Reads the directory given by path and returns an async iterable of `DirEntry`.
|
||||||
* @param path A path to a directory.
|
|
||||||
*/
|
*/
|
||||||
readdir(path: string): Promise<string[]>
|
readDir(path: string): AsyncIterable<DirEntry>
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the path points to a directory.
|
|
||||||
*/
|
|
||||||
isDirectory(path: string): Promise<boolean>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAdapter {
|
/**
|
||||||
|
* Information about a directory entry
|
||||||
|
*/
|
||||||
|
export interface DirEntry {
|
||||||
|
/** The file name of the entry. Does not include the full path. */
|
||||||
|
name: string
|
||||||
|
/** True if this is info for a regular file. */
|
||||||
|
isFile: boolean
|
||||||
|
/** True if this is info for a directory. */
|
||||||
|
isDirectory: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Adapter {
|
||||||
/**
|
/**
|
||||||
* Underlying `fs` module. You can replace it with a wrapper of
|
* Underlying `fs` module. You can replace it with a wrapper of
|
||||||
* `memfs` or any object that implements `IFileSystem`.
|
* `memfs` or any object that implements `IFileSystem`.
|
||||||
*/
|
*/
|
||||||
fs: IFileSystem
|
fs: FileSystem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `SubtleCrypto` implementation. On Node.js this is
|
* `SubtleCrypto` implementation. On Node.js this is
|
||||||
@ -56,19 +59,3 @@ export interface IAdapter {
|
|||||||
*/
|
*/
|
||||||
subtle: SubtleCrypto
|
subtle: SubtleCrypto
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Node.js adapter. This can be used while using `opvault.js`
|
|
||||||
* in a Node.js environment.
|
|
||||||
*/
|
|
||||||
export const nodeAdapter: IAdapter = {
|
|
||||||
fs: {
|
|
||||||
readFile: path => fs.readFile(path, "utf-8"),
|
|
||||||
readBuffer: path => fs.readFile(path),
|
|
||||||
writeFile: fs.writeFile,
|
|
||||||
readdir: fs.readdir,
|
|
||||||
isDirectory: async path => fs.stat(path).then(x => x.isDirectory()),
|
|
||||||
exists: async path => existsSync(path),
|
|
||||||
},
|
|
||||||
subtle: (webcrypto as any).subtle,
|
|
||||||
}
|
|
31
packages/opvault.js/src/adapter/node.ts
Normal file
31
packages/opvault.js/src/adapter/node.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { promises as fs, existsSync } from "fs"
|
||||||
|
import { webcrypto } from "crypto"
|
||||||
|
import { join } from "path"
|
||||||
|
import type { Adapter } from "./index"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Node.js adapter. This can be used while using `opvault.js`
|
||||||
|
* in a Node.js environment.
|
||||||
|
*/
|
||||||
|
export const adapter: Adapter = {
|
||||||
|
fs: {
|
||||||
|
readTextFile: path => fs.readFile(path, "utf-8"),
|
||||||
|
readFile: path => fs.readFile(path),
|
||||||
|
writeTextFile: fs.writeFile,
|
||||||
|
async *readDir(path) {
|
||||||
|
const names = await fs.readdir(path)
|
||||||
|
for (const name of names) {
|
||||||
|
const fullPath = join(path, name)
|
||||||
|
const stat = await fs.stat(fullPath)
|
||||||
|
yield {
|
||||||
|
name,
|
||||||
|
isFile: stat.isFile(),
|
||||||
|
isDirectory: stat.isDirectory(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exists: async path => existsSync(path),
|
||||||
|
},
|
||||||
|
subtle: (webcrypto as any).subtle,
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import { Buffer } from "buffer"
|
import type { Adapter, DirEntry, FileSystem } from "./index"
|
||||||
import type { IAdapter, IFileSystem } from "opvault.js/src/adapter"
|
|
||||||
|
|
||||||
function normalize(path: string) {
|
function normalize(path: string) {
|
||||||
return path.replace(/^\//, "")
|
return path.replace(/^\//, "")
|
||||||
@ -11,7 +10,7 @@ function splitPath(path: string) {
|
|||||||
return [segments, filename] as const
|
return [segments, filename] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileSystem implements IFileSystem {
|
class FS implements FileSystem {
|
||||||
constructor(private handle: FileSystemDirectoryHandle) {}
|
constructor(private handle: FileSystemDirectoryHandle) {}
|
||||||
|
|
||||||
private async getDirectoryHandle(segments: string[]) {
|
private async getDirectoryHandle(segments: string[]) {
|
||||||
@ -33,7 +32,7 @@ export class FileSystem implements IFileSystem {
|
|||||||
return fileHandle
|
return fileHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
async readFile(path: string) {
|
async readTextFile(path: string) {
|
||||||
const handle = await this.getFileHandle(path)
|
const handle = await this.getFileHandle(path)
|
||||||
const file = await handle.getFile()
|
const file = await handle.getFile()
|
||||||
return file.text()
|
return file.text()
|
||||||
@ -60,30 +59,20 @@ export class FileSystem implements IFileSystem {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async readBuffer(path: string): Promise<Buffer> {
|
async readFile(path: string): Promise<Uint8Array> {
|
||||||
const handle = await this.getFileHandle(path)
|
const handle = await this.getFileHandle(path)
|
||||||
const file = await handle.getFile()
|
const file = await handle.getFile()
|
||||||
return Buffer.from(await file.arrayBuffer())
|
return new Uint8Array(await file.arrayBuffer())
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeFile(path: string, data: string): Promise<void> {
|
async writeTextFile(path: string, data: string): Promise<void> {
|
||||||
const handle = await this.getFileHandle(path)
|
const handle = await this.getFileHandle(path)
|
||||||
const writable = await handle.createWritable()
|
const writable = await handle.createWritable()
|
||||||
await writable.write(data)
|
await writable.write(data)
|
||||||
await writable.close()
|
await writable.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
async readdir(path: string): Promise<string[]> {
|
private async isDirectory(path: string) {
|
||||||
const segments = normalize(path).split("/")
|
|
||||||
const dirHandle = await this.getDirectoryHandle(segments)
|
|
||||||
const keys: string[] = []
|
|
||||||
for await (const key of dirHandle.keys()) {
|
|
||||||
keys.push(key)
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
async isDirectory(path: string) {
|
|
||||||
const [segments, filename] = splitPath(path)
|
const [segments, filename] = splitPath(path)
|
||||||
const dirHandle = await this.getDirectoryHandle(segments)
|
const dirHandle = await this.getDirectoryHandle(segments)
|
||||||
for await (const [key, handle] of dirHandle.entries()) {
|
for await (const [key, handle] of dirHandle.entries()) {
|
||||||
@ -97,6 +86,19 @@ export class FileSystem implements IFileSystem {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *readDir(path: string): AsyncIterable<DirEntry> {
|
||||||
|
const segments = normalize(path).split("/")
|
||||||
|
const dirHandle = await this.getDirectoryHandle(segments)
|
||||||
|
for await (const key of dirHandle.keys()) {
|
||||||
|
const isDirectory = await this.isDirectory(`${path}/${key}`)
|
||||||
|
yield {
|
||||||
|
name: key,
|
||||||
|
isDirectory,
|
||||||
|
isFile: !isDirectory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function success(fn: () => Promise<any>) {
|
async function success(fn: () => Promise<any>) {
|
||||||
@ -111,7 +113,7 @@ async function success(fn: () => Promise<any>) {
|
|||||||
/**
|
/**
|
||||||
* Default Browser adapter.
|
* Default Browser adapter.
|
||||||
*/
|
*/
|
||||||
export const getBrowserAdapter = (handle: FileSystemDirectoryHandle): IAdapter => ({
|
export const getBrowserAdapter = (handle: FileSystemDirectoryHandle): Adapter => ({
|
||||||
fs: new FileSystem(handle),
|
fs: new FS(handle),
|
||||||
subtle: crypto.subtle,
|
subtle: crypto.subtle,
|
||||||
})
|
})
|
@ -1,7 +1,7 @@
|
|||||||
import { Buffer } from "buffer"
|
import { Buffer } from "buffer"
|
||||||
import type { IAdapter, IFileSystem } from "opvault.js/src/adapter"
|
import type { Adapter, DirEntry, FileSystem } from "./index"
|
||||||
|
|
||||||
export class FileSystem implements IFileSystem {
|
class FS implements FileSystem {
|
||||||
private paths = new Set<string>()
|
private paths = new Set<string>()
|
||||||
private pathMap = new Map<string, File>()
|
private pathMap = new Map<string, File>()
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ export class FileSystem implements IFileSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readFile(path: string) {
|
async readTextFile(path: string) {
|
||||||
return this.pathMap.get(path)!.text()
|
return this.pathMap.get(path)!.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,34 +20,40 @@ export class FileSystem implements IFileSystem {
|
|||||||
return this.pathMap.has(path)
|
return this.pathMap.has(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async readBuffer(path: string): Promise<Buffer> {
|
async readFile(path: string): Promise<Buffer> {
|
||||||
const arrayBuffer = await this.pathMap.get(path)!.arrayBuffer()
|
const arrayBuffer = await this.pathMap.get(path)!.arrayBuffer()
|
||||||
return Buffer.from(arrayBuffer)
|
return Buffer.from(arrayBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
async writeFile(path: string, data: string): Promise<void> {
|
async writeTextFile(): Promise<void> {
|
||||||
throw new Error("fs.writeFile is not supported with webkitdirectory")
|
throw new Error("fs.writeFile is not supported with webkitdirectory")
|
||||||
}
|
}
|
||||||
|
|
||||||
async readdir(path: string): Promise<string[]> {
|
private isDirectory(path: string) {
|
||||||
const paths = [...this.paths]
|
|
||||||
return paths
|
|
||||||
.filter(_ => _.startsWith(`${path}/`))
|
|
||||||
.map(_ => _.slice(path.length + 1))
|
|
||||||
.map(_ => _.split("/")[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
async isDirectory(path: string) {
|
|
||||||
const paths = [...this.paths]
|
const paths = [...this.paths]
|
||||||
return paths.some(_ => _.startsWith(`${path}/`)) && !paths.includes(path)
|
return paths.some(_ => _.startsWith(`${path}/`)) && !paths.includes(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async *readDir(path: string): AsyncIterable<DirEntry> {
|
||||||
|
for (const name of [...this.paths]
|
||||||
|
.filter(_ => _.startsWith(`${path}/`))
|
||||||
|
.map(_ => _.slice(path.length + 1))
|
||||||
|
.map(_ => _.split("/")[0])) {
|
||||||
|
const isDirectory = this.isDirectory(path)
|
||||||
|
yield {
|
||||||
|
name,
|
||||||
|
isDirectory,
|
||||||
|
isFile: !isDirectory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default Browser adapter.
|
* Default Browser adapter.
|
||||||
*/
|
*/
|
||||||
export const getBrowserAdapter = (list: FileList): IAdapter => ({
|
export const getBrowserAdapter = (list: FileList): Adapter => ({
|
||||||
fs: new FileSystem(list),
|
fs: new FS(list),
|
||||||
subtle: crypto.subtle,
|
subtle: crypto.subtle,
|
||||||
})
|
})
|
362
packages/opvault.js/src/buffer.ts
Normal file
362
packages/opvault.js/src/buffer.ts
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
/**
|
||||||
|
* The buffer module from node.js, for the browser.
|
||||||
|
*
|
||||||
|
* @author Feross Aboukhadijeh <https://feross.org>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
// The MIT License (MIT)
|
||||||
|
// Copyright (c) 2014 Jameson Little
|
||||||
|
|
||||||
|
const lookup: string[] = []
|
||||||
|
const revLookup: number[] = []
|
||||||
|
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
for (let i = 0, len = alphabet.length; i < len; ++i) {
|
||||||
|
lookup[i] = alphabet[i]
|
||||||
|
revLookup[alphabet.charCodeAt(i)] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support decoding URL-safe base64 strings, as Node.js does.
|
||||||
|
// See: https://en.wikipedia.org/wiki/Base64#URL_applications
|
||||||
|
revLookup[45] = 62
|
||||||
|
revLookup[95] = 63
|
||||||
|
|
||||||
|
function base64ToByteArray(b64: string): Uint8Array {
|
||||||
|
const { length } = b64
|
||||||
|
|
||||||
|
if (length % 4 > 0) {
|
||||||
|
throw new Error("Invalid string. Length must be a multiple of 4")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim off extra bytes after placeholder bytes are found
|
||||||
|
// See: https://github.com/beatgammit/base64-js/issues/42
|
||||||
|
let validLen = b64.indexOf("=")
|
||||||
|
if (validLen === -1) validLen = length
|
||||||
|
|
||||||
|
const placeHoldersLen = validLen === length ? 0 : 4 - (validLen % 4)
|
||||||
|
|
||||||
|
const arr = new Uint8Array(((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen)
|
||||||
|
|
||||||
|
let curByte = 0
|
||||||
|
|
||||||
|
// if there are placeholders, only get up to the last complete 4 chars
|
||||||
|
const len = placeHoldersLen > 0 ? validLen - 4 : validLen
|
||||||
|
|
||||||
|
let i: number
|
||||||
|
for (i = 0; i < len; i += 4) {
|
||||||
|
const tmp =
|
||||||
|
(revLookup[b64.charCodeAt(i)] << 18) |
|
||||||
|
(revLookup[b64.charCodeAt(i + 1)] << 12) |
|
||||||
|
(revLookup[b64.charCodeAt(i + 2)] << 6) |
|
||||||
|
revLookup[b64.charCodeAt(i + 3)]
|
||||||
|
arr[curByte++] = (tmp >> 16) & 0xff
|
||||||
|
arr[curByte++] = (tmp >> 8) & 0xff
|
||||||
|
arr[curByte++] = tmp & 0xff
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeHoldersLen === 2) {
|
||||||
|
const tmp =
|
||||||
|
(revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4)
|
||||||
|
arr[curByte++] = tmp & 0xff
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeHoldersLen === 1) {
|
||||||
|
const tmp =
|
||||||
|
(revLookup[b64.charCodeAt(i)] << 10) |
|
||||||
|
(revLookup[b64.charCodeAt(i + 1)] << 4) |
|
||||||
|
(revLookup[b64.charCodeAt(i + 2)] >> 2)
|
||||||
|
arr[curByte++] = (tmp >> 8) & 0xff
|
||||||
|
arr[curByte++] = tmp & 0xff
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
const tripletToBase64 = (num: number) =>
|
||||||
|
lookup[(num >> 18) & 0x3f] +
|
||||||
|
lookup[(num >> 12) & 0x3f] +
|
||||||
|
lookup[(num >> 6) & 0x3f] +
|
||||||
|
lookup[num & 0x3f]
|
||||||
|
|
||||||
|
function encodeChunk(uint8: Uint8Array, start: number, end: number) {
|
||||||
|
const output: string[] = []
|
||||||
|
for (let i = start; i < end; i += 3) {
|
||||||
|
const tmp =
|
||||||
|
((uint8[i] << 16) & 0xff0000) +
|
||||||
|
((uint8[i + 1] << 8) & 0xff00) +
|
||||||
|
(uint8[i + 2] & 0xff)
|
||||||
|
output.push(tripletToBase64(tmp))
|
||||||
|
}
|
||||||
|
return output.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64FromByteArray(uint8: Uint8Array): string {
|
||||||
|
let tmp: number
|
||||||
|
const len = uint8.length
|
||||||
|
const extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes
|
||||||
|
const parts = []
|
||||||
|
const maxChunkLength = 16383 // must be multiple of 3
|
||||||
|
|
||||||
|
// go through the array every three bytes, we'll deal with trailing stuff later
|
||||||
|
for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
|
||||||
|
parts.push(
|
||||||
|
encodeChunk(uint8, i, i + maxChunkLength > len2 ? len2 : i + maxChunkLength)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad the end with zeros, but make sure to not forget the extra bytes
|
||||||
|
if (extraBytes === 1) {
|
||||||
|
tmp = uint8[len - 1]
|
||||||
|
parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + "==")
|
||||||
|
} else if (extraBytes === 2) {
|
||||||
|
tmp = (uint8[len - 2] << 8) + uint8[len - 1]
|
||||||
|
parts.push(
|
||||||
|
lookup[tmp >> 10] + lookup[(tmp >> 4) & 0x3f] + lookup[(tmp << 2) & 0x3f] + "="
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromBase64(string: string) {
|
||||||
|
const { length } = base64ToBytes(string)
|
||||||
|
let buf = new Uint8Array(length)
|
||||||
|
|
||||||
|
const actual = write(buf, string)
|
||||||
|
|
||||||
|
if (actual !== length) {
|
||||||
|
// Writing a hex string, for example, that contains invalid characters will
|
||||||
|
// cause everything after the first invalid character to be ignored. (e.g.
|
||||||
|
// 'abxxcd' will be treated as 'ab')
|
||||||
|
buf = buf.slice(0, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(array: Uint8Array, string: string) {
|
||||||
|
const offset = 0
|
||||||
|
let { length } = array
|
||||||
|
const remaining = array.length - offset
|
||||||
|
if (length === undefined || length > remaining) length = remaining
|
||||||
|
|
||||||
|
if ((string.length > 0 && (length < 0 || offset < 0)) || offset > array.length) {
|
||||||
|
throw new RangeError("Attempt to write outside buffer bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
return blitBuffer(base64ToBytes(string), array, offset, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function utf8Slice(array: Uint8Array, start = 0, end = array.length) {
|
||||||
|
end = Math.min(array.length, end)
|
||||||
|
const res: number[] = []
|
||||||
|
|
||||||
|
let i = start
|
||||||
|
while (i < end) {
|
||||||
|
const firstByte = array[i]
|
||||||
|
let codePoint = null
|
||||||
|
let bytesPerSequence =
|
||||||
|
firstByte > 0xef ? 4 : firstByte > 0xdf ? 3 : firstByte > 0xbf ? 2 : 1
|
||||||
|
|
||||||
|
if (i + bytesPerSequence <= end) {
|
||||||
|
let secondByte: number
|
||||||
|
let thirdByte: number
|
||||||
|
let fourthByte: number
|
||||||
|
let tempCodePoint: number
|
||||||
|
|
||||||
|
switch (bytesPerSequence) {
|
||||||
|
case 1:
|
||||||
|
if (firstByte < 0x80) {
|
||||||
|
codePoint = firstByte
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
secondByte = array[i + 1]
|
||||||
|
if ((secondByte & 0xc0) === 0x80) {
|
||||||
|
tempCodePoint = ((firstByte & 0x1f) << 0x6) | (secondByte & 0x3f)
|
||||||
|
if (tempCodePoint > 0x7f) {
|
||||||
|
codePoint = tempCodePoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
secondByte = array[i + 1]
|
||||||
|
thirdByte = array[i + 2]
|
||||||
|
if ((secondByte & 0xc0) === 0x80 && (thirdByte & 0xc0) === 0x80) {
|
||||||
|
tempCodePoint =
|
||||||
|
((firstByte & 0xf) << 0xc) |
|
||||||
|
((secondByte & 0x3f) << 0x6) |
|
||||||
|
(thirdByte & 0x3f)
|
||||||
|
if (
|
||||||
|
tempCodePoint > 0x7ff &&
|
||||||
|
(tempCodePoint < 0xd800 || tempCodePoint > 0xdfff)
|
||||||
|
) {
|
||||||
|
codePoint = tempCodePoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
secondByte = array[i + 1]
|
||||||
|
thirdByte = array[i + 2]
|
||||||
|
fourthByte = array[i + 3]
|
||||||
|
if (
|
||||||
|
(secondByte & 0xc0) === 0x80 &&
|
||||||
|
(thirdByte & 0xc0) === 0x80 &&
|
||||||
|
(fourthByte & 0xc0) === 0x80
|
||||||
|
) {
|
||||||
|
tempCodePoint =
|
||||||
|
((firstByte & 0xf) << 0x12) |
|
||||||
|
((secondByte & 0x3f) << 0xc) |
|
||||||
|
((thirdByte & 0x3f) << 0x6) |
|
||||||
|
(fourthByte & 0x3f)
|
||||||
|
if (tempCodePoint > 0xffff && tempCodePoint < 0x110000) {
|
||||||
|
codePoint = tempCodePoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codePoint === null) {
|
||||||
|
// we did not generate a valid codePoint so insert a
|
||||||
|
// replacement char (U+FFFD) and advance only 1 byte
|
||||||
|
codePoint = 0xfffd
|
||||||
|
bytesPerSequence = 1
|
||||||
|
} else if (codePoint > 0xffff) {
|
||||||
|
// encode to utf16 (surrogate pair dance)
|
||||||
|
codePoint -= 0x10000
|
||||||
|
res.push(((codePoint >>> 10) & 0x3ff) | 0xd800)
|
||||||
|
codePoint = 0xdc00 | (codePoint & 0x3ff)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push(codePoint)
|
||||||
|
i += bytesPerSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeCodePointsArray(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on http://stackoverflow.com/a/22747272/680742, the browser with
|
||||||
|
// the lowest limit is Chrome, with 0x10000 args.
|
||||||
|
// We go 1 magnitude less, for safety
|
||||||
|
const MAX_ARGUMENTS_LENGTH = 0x1000
|
||||||
|
|
||||||
|
function decodeCodePointsArray(codePoints: number[]) {
|
||||||
|
const len = codePoints.length
|
||||||
|
if (len <= MAX_ARGUMENTS_LENGTH) {
|
||||||
|
return String.fromCharCode(...codePoints) // avoid extra slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode in chunks to avoid "call stack size exceeded".
|
||||||
|
let res = ""
|
||||||
|
let i = 0
|
||||||
|
while (i < len) {
|
||||||
|
res += String.fromCharCode(...codePoints.slice(i, (i += MAX_ARGUMENTS_LENGTH)))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Need to make sure that buffer isn't trying to write out of bounds.
|
||||||
|
*/
|
||||||
|
function checkOffset(offset: number, ext: number, length: number) {
|
||||||
|
if (offset % 1 !== 0 || offset < 0) throw new RangeError("offset is not uint")
|
||||||
|
if (offset + ext > length) throw new RangeError("Trying to access beyond buffer length")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readUInt32BE(array: Uint8Array, offset: number, noAssert?: boolean) {
|
||||||
|
offset = offset >>> 0
|
||||||
|
if (!noAssert) checkOffset(offset, 4, array.length)
|
||||||
|
|
||||||
|
return (
|
||||||
|
array[offset] * 0x1000000 +
|
||||||
|
((array[offset + 1] << 16) | (array[offset + 2] << 8) | array[offset + 3])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readIntLE(
|
||||||
|
array: Uint8Array,
|
||||||
|
offset: number,
|
||||||
|
byteLength: number,
|
||||||
|
noAssert?: boolean
|
||||||
|
) {
|
||||||
|
offset = offset >>> 0
|
||||||
|
byteLength = byteLength >>> 0
|
||||||
|
if (!noAssert) checkOffset(offset, byteLength, array.length)
|
||||||
|
|
||||||
|
let val = array[offset]
|
||||||
|
let mul = 1
|
||||||
|
let i = 0
|
||||||
|
while (++i < byteLength && (mul *= 0x100)) {
|
||||||
|
val += array[offset + i] * mul
|
||||||
|
}
|
||||||
|
mul *= 0x80
|
||||||
|
|
||||||
|
if (val >= mul) val -= Math.pow(2, 8 * byteLength)
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkInt(
|
||||||
|
buf: Uint8Array,
|
||||||
|
value: number,
|
||||||
|
offset: number,
|
||||||
|
ext: number,
|
||||||
|
max: number,
|
||||||
|
min: number
|
||||||
|
) {
|
||||||
|
if (value > max || value < min) {
|
||||||
|
throw new RangeError('"value" argument is out of bounds')
|
||||||
|
}
|
||||||
|
if (offset + ext > buf.length) {
|
||||||
|
throw new RangeError("Index out of range")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeUInt32BE(
|
||||||
|
array: Uint8Array,
|
||||||
|
value: number,
|
||||||
|
offset: number,
|
||||||
|
noAssert?: boolean
|
||||||
|
) {
|
||||||
|
value = +value
|
||||||
|
offset = offset >>> 0
|
||||||
|
if (!noAssert) checkInt(array, value, offset, 4, 0xffffffff, 0)
|
||||||
|
array[offset] = value >>> 24
|
||||||
|
array[offset + 1] = value >>> 16
|
||||||
|
array[offset + 2] = value >>> 8
|
||||||
|
array[offset + 3] = value & 0xff
|
||||||
|
return offset + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ================
|
||||||
|
|
||||||
|
const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g
|
||||||
|
|
||||||
|
function base64ToBytes(str: string) {
|
||||||
|
// Node takes equal signs as end of the Base64 encoding
|
||||||
|
;[str] = str.split("=")
|
||||||
|
// Node strips out invalid characters like \n and \t from the string, base64-js does not
|
||||||
|
str = str.trim().replace(INVALID_BASE64_RE, "")
|
||||||
|
// Node converts strings with length < 2 to ''
|
||||||
|
if (str.length < 2) return new Uint8Array()
|
||||||
|
// Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
|
||||||
|
while (str.length % 4 !== 0) {
|
||||||
|
str = str + "="
|
||||||
|
}
|
||||||
|
return base64ToByteArray(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
function blitBuffer(
|
||||||
|
src: Uint8Array | number[],
|
||||||
|
dst: Uint8Array,
|
||||||
|
offset: number,
|
||||||
|
length: number
|
||||||
|
) {
|
||||||
|
let i: number
|
||||||
|
for (i = 0; i < length; ++i) {
|
||||||
|
if (i + offset >= dst.length || i >= src.length) break
|
||||||
|
dst[i + offset] = src[i]
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
@ -1,19 +1,18 @@
|
|||||||
import { Buffer } from "buffer"
|
|
||||||
import { decryptData } from "./decipher"
|
import { decryptData } from "./decipher"
|
||||||
import type { IAdapter } from "./adapter"
|
import type { Adapter } from "./adapter"
|
||||||
import { createEventEmitter } from "./ee"
|
import { createEventEmitter } from "./ee"
|
||||||
import { HMACAssertionError } from "./errors"
|
import { HMACAssertionError, OPVaultError } from "./errors"
|
||||||
import type { i18n } from "./i18n"
|
|
||||||
import type { ItemDetails, Overview, Profile } from "./types"
|
import type { ItemDetails, Overview, Profile } from "./types"
|
||||||
import { setIfAbsent } from "./util"
|
import { setIfAbsent } from "./util"
|
||||||
import type { EncryptedItem } from "./models/item"
|
import type { EncryptedItem } from "./models/item"
|
||||||
|
import { fromBase64, utf8Slice } from "./buffer"
|
||||||
|
|
||||||
/** Encryption and MAC */
|
/** Encryption and MAC */
|
||||||
export interface Cipher {
|
export interface Cipher {
|
||||||
/** Encryption key */
|
/** Encryption key */
|
||||||
key: Buffer
|
key: Uint8Array
|
||||||
/** HMAC key */
|
/** HMAC key */
|
||||||
hmac: Buffer
|
hmac: Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Crypto {
|
export class Crypto {
|
||||||
@ -27,7 +26,7 @@ export class Crypto {
|
|||||||
|
|
||||||
readonly onLock = createEventEmitter<void>()
|
readonly onLock = createEventEmitter<void>()
|
||||||
|
|
||||||
constructor(private readonly i18n: i18n, adapter: IAdapter) {
|
constructor(adapter: Adapter) {
|
||||||
this.subtle = adapter.subtle
|
this.subtle = adapter.subtle
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ export class Crypto {
|
|||||||
const derivedKey = await this.subtle.deriveBits(
|
const derivedKey = await this.subtle.deriveBits(
|
||||||
{
|
{
|
||||||
name: "PBKDF2",
|
name: "PBKDF2",
|
||||||
salt: Buffer.from(profile.salt, "base64"),
|
salt: fromBase64(profile.salt),
|
||||||
iterations: profile.iterations,
|
iterations: profile.iterations,
|
||||||
hash: {
|
hash: {
|
||||||
name: "SHA-512",
|
name: "SHA-512",
|
||||||
@ -53,7 +52,7 @@ export class Crypto {
|
|||||||
64 << 3
|
64 << 3
|
||||||
)
|
)
|
||||||
|
|
||||||
const cipher = splitPlainText(Buffer.from(derivedKey))
|
const cipher = splitPlainText(new Uint8Array(derivedKey))
|
||||||
|
|
||||||
// Derive master key and overview keys
|
// Derive master key and overview keys
|
||||||
this.#master = await this.decryptKeys(profile.masterKey, cipher)
|
this.#master = await this.decryptKeys(profile.masterKey, cipher)
|
||||||
@ -73,13 +72,9 @@ export class Crypto {
|
|||||||
this.onLock()
|
this.onLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertUnlocked() {
|
assertUnlocked() {
|
||||||
if (this.#locked) {
|
if (this.#locked) {
|
||||||
throw new Error(this.i18n.error.vaultIsLocked)
|
throw new OPVaultError("This vault is locked", "VAULT_LOCKED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,22 +97,22 @@ export class Crypto {
|
|||||||
|
|
||||||
decryptItemDetails = this.#createWeakCache(async (item: EncryptedItem) => {
|
decryptItemDetails = this.#createWeakCache(async (item: EncryptedItem) => {
|
||||||
const cipher = await this.deriveConcreteKey(item)
|
const cipher = await this.deriveConcreteKey(item)
|
||||||
const detail = await this.decryptOPData(Buffer.from(item.d, "base64"), cipher)
|
const detail = await this.decryptOPData(fromBase64(item.d), cipher)
|
||||||
return JSON.parse(detail.toString("utf-8")) as ItemDetails
|
return JSON.parse(utf8Slice(detail)) as ItemDetails
|
||||||
})
|
})
|
||||||
|
|
||||||
decryptItemOverview = this.#createCache(
|
decryptItemOverview = this.#createCache(
|
||||||
(item: EncryptedItem) => item.o,
|
(item: EncryptedItem) => item.o,
|
||||||
async (o: string) => {
|
async (o: string) => {
|
||||||
const overview = await this.decryptOPData(Buffer.from(o, "base64"), this.#overview)
|
const overview = await this.decryptOPData(fromBase64(o), this.#overview)
|
||||||
return JSON.parse(overview.toString("utf8")) as Overview
|
return JSON.parse(utf8Slice(overview)) as Overview
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
deriveConcreteKey = this.#createCache(
|
deriveConcreteKey = this.#createCache(
|
||||||
(data: { k: string }) => data.k,
|
(data: { k: string }) => data.k,
|
||||||
async ($k: string) => {
|
async ($k: string) => {
|
||||||
const k = Buffer.from($k, "base64")
|
const k = fromBase64($k)
|
||||||
const data = k.slice(0, -32)
|
const data = k.slice(0, -32)
|
||||||
await this.assertHMac(data, this.#master.hmac, k.slice(-32))
|
await this.assertHMac(data, this.#master.hmac, k.slice(-32))
|
||||||
const derivedKey = await this.decryptData(
|
const derivedKey = await this.decryptData(
|
||||||
@ -129,7 +124,7 @@ export class Crypto {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async assertHMac(data: Buffer, key: Buffer, expected: Buffer) {
|
async assertHMac(data: Uint8Array, key: Uint8Array, expected: Uint8Array) {
|
||||||
const cryptoKey = await this.subtle.importKey(
|
const cryptoKey = await this.subtle.importKey(
|
||||||
"raw",
|
"raw",
|
||||||
key,
|
key,
|
||||||
@ -148,7 +143,7 @@ export class Crypto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptOPData(cipherText: Buffer, cipher: Cipher) {
|
async decryptOPData(cipherText: Uint8Array, cipher: Cipher) {
|
||||||
const key = cipherText.slice(0, -32)
|
const key = cipherText.slice(0, -32)
|
||||||
await this.assertHMac(key, cipher.hmac, cipherText.slice(-32))
|
await this.assertHMac(key, cipher.hmac, cipherText.slice(-32))
|
||||||
|
|
||||||
@ -157,17 +152,28 @@ export class Crypto {
|
|||||||
return plaintext.slice(-size)
|
return plaintext.slice(-size)
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptData(key: Buffer, iv: Buffer, data: Buffer) {
|
async decryptData(key: Uint8Array, iv: Uint8Array, data: Uint8Array) {
|
||||||
this.subtle
|
// try {
|
||||||
// return createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false).update(data)
|
// const algorithm = { name: "AES-CBC", length: 256, iv }
|
||||||
|
// const keyCrypto = await this.subtle.importKey("raw", key, algorithm, false, [
|
||||||
|
// "decrypt",
|
||||||
|
// ])
|
||||||
|
// console.log("hi", keyCrypto)
|
||||||
|
// const decrypted = await this.subtle.decrypt(algorithm, keyCrypto, data)
|
||||||
|
// console.log("decrypted")
|
||||||
|
// return Buffer.from(decrypted)
|
||||||
|
// // return createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false).update(data)
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error(e)
|
||||||
return decryptData(key, iv, data)
|
return decryptData(key, iv, data)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptKeys(encryptedKey: string, derived: Cipher) {
|
async decryptKeys(encryptedKey: string, derived: Cipher) {
|
||||||
const buffer = Buffer.from(encryptedKey, "base64")
|
const buffer = fromBase64(encryptedKey)
|
||||||
const base = await this.decryptOPData(buffer, derived)
|
const base = await this.decryptOPData(buffer, derived)
|
||||||
const digest = await this.subtle.digest("SHA-512", base)
|
const digest = await this.subtle.digest("SHA-512", base)
|
||||||
return splitPlainText(Buffer.from(digest))
|
return splitPlainText(new Uint8Array(digest))
|
||||||
}
|
}
|
||||||
|
|
||||||
get overview() {
|
get overview() {
|
||||||
@ -175,11 +181,11 @@ export class Crypto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const splitPlainText = (derivedKey: Buffer): Cipher => ({
|
export const splitPlainText = (derivedKey: Uint8Array): Cipher => ({
|
||||||
key: derivedKey.slice(0, 32),
|
key: derivedKey.slice(0, 32),
|
||||||
hmac: derivedKey.slice(32, 64),
|
hmac: derivedKey.slice(32, 64),
|
||||||
})
|
})
|
||||||
|
|
||||||
function readUint16({ buffer, byteOffset, length }: Buffer) {
|
function readUint16({ buffer, byteOffset, length }: Uint8Array) {
|
||||||
return new DataView(buffer, byteOffset, length).getUint16(0, true)
|
return new DataView(buffer, byteOffset, length).getUint16(0, true)
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,12 @@
|
|||||||
* | MIT | Crypto-js | (c) 2009-2013 Jeff Mott. |
|
* | MIT | Crypto-js | (c) 2009-2013 Jeff Mott. |
|
||||||
* | MIT | browserify-aes | (c) 2014-2017 browserify-aes contributors |
|
* | MIT | browserify-aes | (c) 2014-2017 browserify-aes contributors |
|
||||||
*/
|
*/
|
||||||
import { Buffer } from "buffer"
|
import invariant from "tiny-invariant"
|
||||||
|
import { readUInt32BE, writeUInt32BE } from "./buffer"
|
||||||
|
|
||||||
function bufferXor(a: Buffer, b: Buffer) {
|
function bufferXor(a: Uint8Array, b: Uint8Array) {
|
||||||
const length = Math.min(a.length, b.length)
|
const length = Math.min(a.length, b.length)
|
||||||
const buffer = Buffer.alloc(length)
|
const buffer = new Uint8Array(length)
|
||||||
|
|
||||||
for (let i = 0; i < length; ++i) {
|
for (let i = 0; i < length; ++i) {
|
||||||
buffer[i] = a[i] ^ b[i]
|
buffer[i] = a[i] ^ b[i]
|
||||||
@ -20,12 +21,12 @@ function bufferXor(a: Buffer, b: Buffer) {
|
|||||||
return buffer
|
return buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
function toUInt32Array(buf: Buffer) {
|
function toUInt32Array(buf: Uint8Array) {
|
||||||
const len = (buf.length / 4) | 0
|
const len = (buf.length / 4) | 0
|
||||||
const out: number[] = new Array(len)
|
const out: number[] = new Array(len)
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
out[i] = buf.readUInt32BE(i * 4)
|
out[i] = readUInt32BE(buf, i * 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
@ -177,7 +178,7 @@ class AES {
|
|||||||
private nRounds!: number
|
private nRounds!: number
|
||||||
private invKeySchedule!: number[]
|
private invKeySchedule!: number[]
|
||||||
|
|
||||||
constructor(key: Buffer) {
|
constructor(key: Uint8Array) {
|
||||||
this.key = toUInt32Array(key)
|
this.key = toUInt32Array(key)
|
||||||
this.reset()
|
this.reset()
|
||||||
}
|
}
|
||||||
@ -236,7 +237,7 @@ class AES {
|
|||||||
this.invKeySchedule = invKeySchedule
|
this.invKeySchedule = invKeySchedule
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptBlock(buffer: Buffer) {
|
decryptBlock(buffer: Uint8Array) {
|
||||||
const M = toUInt32Array(buffer)
|
const M = toUInt32Array(buffer)
|
||||||
|
|
||||||
// swap
|
// swap
|
||||||
@ -246,11 +247,11 @@ class AES {
|
|||||||
|
|
||||||
const out = cryptBlock(M, this.invKeySchedule, G.invSubMix, G.invSBox, this.nRounds)
|
const out = cryptBlock(M, this.invKeySchedule, G.invSubMix, G.invSBox, this.nRounds)
|
||||||
|
|
||||||
const buf = Buffer.allocUnsafe(16)
|
const buf = new Uint8Array(16)
|
||||||
buf.writeUInt32BE(out[0], 0)
|
writeUInt32BE(buf, out[0], 0)
|
||||||
buf.writeUInt32BE(out[3], 4)
|
writeUInt32BE(buf, out[3], 4)
|
||||||
buf.writeUInt32BE(out[2], 8)
|
writeUInt32BE(buf, out[2], 8)
|
||||||
buf.writeUInt32BE(out[1], 12)
|
writeUInt32BE(buf, out[1], 12)
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,44 +259,42 @@ class AES {
|
|||||||
static keySize = 256 / 8
|
static keySize = 256 / 8
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitter() {
|
const splitter = (data: Uint8Array) => () => {
|
||||||
let cache = Buffer.allocUnsafe(0)
|
if (data.length >= 16) {
|
||||||
return {
|
const out = data.slice(0, 16)
|
||||||
add(data: Buffer) {
|
data = data.slice(16)
|
||||||
cache = Buffer.concat([cache, data])
|
return out
|
||||||
return this
|
|
||||||
},
|
|
||||||
get() {
|
|
||||||
if (cache.length >= 16) {
|
|
||||||
const out = cache.slice(0, 16)
|
|
||||||
cache = cache.slice(16)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// AES-256-CBC
|
// AES-256-CBC
|
||||||
// == createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false).update(data)
|
// == createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false).update(data)
|
||||||
export function decryptData(key: Buffer, iv: Buffer, data: Buffer) {
|
export function decryptData(key: Uint8Array, iv: Uint8Array, data: Uint8Array) {
|
||||||
if (iv.length !== 16) {
|
invariant(iv.length === 16, `invalid iv length ${iv.length}`)
|
||||||
throw new TypeError(`invalid iv length ${iv.length}`)
|
invariant(key.length === 32, `invalid key length ${key.length}`)
|
||||||
}
|
|
||||||
if (key.length !== 32) {
|
|
||||||
throw new TypeError(`invalid key length ${key.length}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cipher = new AES(key)
|
const cipher = new AES(key)
|
||||||
let prev = Buffer.from(iv)
|
let prev = iv
|
||||||
const cache = splitter().add(data)
|
const readChunk = splitter(data)
|
||||||
let chunk: Buffer | null
|
let chunk: Uint8Array | null
|
||||||
const res: Buffer[] = []
|
const res: Uint8Array[] = []
|
||||||
while ((chunk = cache.get())) {
|
let totalLength = 0
|
||||||
|
|
||||||
|
while ((chunk = readChunk())) {
|
||||||
const pad = prev
|
const pad = prev
|
||||||
prev = chunk
|
prev = chunk
|
||||||
const out = cipher.decryptBlock(chunk)
|
const out = cipher.decryptBlock(chunk)
|
||||||
res.push(bufferXor(out, pad))
|
const array = bufferXor(out, pad)
|
||||||
|
res.push(array)
|
||||||
|
totalLength += array.length
|
||||||
}
|
}
|
||||||
return Buffer.concat(res)
|
|
||||||
|
const result = new Uint8Array(totalLength)
|
||||||
|
let offset = 0
|
||||||
|
for (const array of res) {
|
||||||
|
result.set(array, offset)
|
||||||
|
offset += array.length
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,6 @@ export function createEventEmitter<T = void>() {
|
|||||||
function emitter(value: T | EventListener) {
|
function emitter(value: T | EventListener) {
|
||||||
if (typeof value === "function") {
|
if (typeof value === "function") {
|
||||||
listeners.add(value as EventListener)
|
listeners.add(value as EventListener)
|
||||||
return () => {
|
|
||||||
listeners.delete(value as EventListener)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
listeners.forEach(fn => fn(value))
|
listeners.forEach(fn => fn(value))
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
export abstract class OPVaultError extends Error {}
|
export class OPVaultError extends Error {
|
||||||
|
constructor(message?: string, readonly code?: string) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class AssertionError extends OPVaultError {}
|
class AssertionError extends OPVaultError {}
|
||||||
|
|
||||||
export class HMACAssertionError extends AssertionError {}
|
export class HMACAssertionError extends AssertionError {}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { resolve, extname, basename } from "path"
|
import { resolve, extname, basename } from "path"
|
||||||
import invariant from "tiny-invariant"
|
import invariant from "tiny-invariant"
|
||||||
import type { IFileSystem } from "./adapter"
|
import type { FileSystem } from "./adapter"
|
||||||
import { once } from "./util"
|
import { once } from "./util"
|
||||||
|
|
||||||
export type OnePasswordFileManager = ReturnType<typeof OnePasswordFileManager>
|
export type OnePasswordFileManager = ReturnType<typeof OnePasswordFileManager>
|
||||||
|
|
||||||
export async function OnePasswordFileManager(
|
export async function OnePasswordFileManager(
|
||||||
fs: IFileSystem,
|
fs: FileSystem,
|
||||||
path: string,
|
path: string,
|
||||||
profileName: string
|
profileName: string
|
||||||
) {
|
) {
|
||||||
@ -18,49 +18,48 @@ export async function OnePasswordFileManager(
|
|||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
getProfile() {
|
getProfile() {
|
||||||
return fs.readFile(abs("profile.js"))
|
return fs.readTextFile(abs("profile.js"))
|
||||||
},
|
},
|
||||||
|
|
||||||
getFolders() {
|
getFolders() {
|
||||||
return fs.readFile(abs("folders.js"))
|
return fs.readTextFile(abs("folders.js"))
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAttachments() {
|
async *getAttachments() {
|
||||||
const files = await fs.readdir(root)
|
for await (const { name } of fs.readDir(root)) {
|
||||||
return files
|
if (extname(name) !== ".attachment") continue
|
||||||
.filter(name => extname(name) === ".attachment")
|
|
||||||
.map(name => {
|
const sep = name.indexOf("_")
|
||||||
const sep = name.indexOf("_")
|
const path = resolve(root, name)
|
||||||
const path = resolve(root, name)
|
const [itemUUID, fileUUID] = [
|
||||||
const [itemUUID, fileUUID] = [
|
name.slice(0, sep),
|
||||||
name.slice(0, sep),
|
basename(name.slice(sep + 1), extname(name)),
|
||||||
basename(name.slice(sep + 1), extname(name)),
|
]
|
||||||
]
|
yield {
|
||||||
return {
|
itemUUID,
|
||||||
itemUUID,
|
fileUUID,
|
||||||
fileUUID,
|
getFile: once(() => fs.readFile(path)),
|
||||||
getFile: once(() => fs.readBuffer(path)),
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getBand(name: string) {
|
async getBand(name: string) {
|
||||||
const path = abs(`band_${name}.js`)
|
const path = abs(`band_${name}.js`)
|
||||||
if (await fs.exists(path)) {
|
if (await fs.exists(path)) {
|
||||||
return await fs.readFile(path)
|
return await fs.readTextFile(path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async setProfile(profile: string) {
|
async setProfile(profile: string) {
|
||||||
await fs.writeFile("profile.js", profile)
|
await fs.writeTextFile("profile.js", profile)
|
||||||
},
|
},
|
||||||
|
|
||||||
async setFolders(folders: string) {
|
async setFolders(folders: string) {
|
||||||
await fs.writeFile("folders.js", folders)
|
await fs.writeTextFile("folders.js", folders)
|
||||||
},
|
},
|
||||||
|
|
||||||
async setBand(name: string, band: string) {
|
async setBand(name: string, band: string) {
|
||||||
await fs.writeFile(`band_${name}.js`, band)
|
await fs.writeTextFile(`band_${name}.js`, band)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import json from "./res.json"
|
|
||||||
|
|
||||||
const [locale] = Intl.DateTimeFormat().resolvedOptions().locale.split("-")
|
|
||||||
|
|
||||||
const mapValue = <T, R>(
|
|
||||||
object: Record<string, T>,
|
|
||||||
fn: (value: T, key: string) => R
|
|
||||||
): Record<string, R> => {
|
|
||||||
const res = Object.create(null)
|
|
||||||
Object.entries(object).forEach(([key, value]) => {
|
|
||||||
res[key] = fn(value, key)
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
type json = typeof json
|
|
||||||
export type i18n = {
|
|
||||||
[K in keyof json]: {
|
|
||||||
[L in keyof json[K]]: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const i18n: i18n = mapValue(json, dict =>
|
|
||||||
mapValue(dict, (value: any) => value[locale] ?? value.en)
|
|
||||||
) as any
|
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"error": {
|
|
||||||
"invalidPassword": {
|
|
||||||
"en": "Invalid password",
|
|
||||||
"fr": "Mot de passe invalide"
|
|
||||||
},
|
|
||||||
"vaultIsLocked": {
|
|
||||||
"en": "This vault is locked",
|
|
||||||
"fr": "Ce coffre est verrouillé."
|
|
||||||
},
|
|
||||||
"cannotDecryptOverviewItem": {
|
|
||||||
"en": "Failed to decrypt overview item",
|
|
||||||
"fr": "Impossible de déchiffrer cet aperçu"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
import { resolve } from "path"
|
import { resolve } from "path"
|
||||||
import { Vault } from "./models/vault"
|
import { Vault } from "./models/vault"
|
||||||
import type { IAdapter } from "./adapter"
|
import type { Adapter } from "./adapter"
|
||||||
import { asyncMap } from "./util"
|
|
||||||
|
|
||||||
export type { Vault } from "./models/vault"
|
export type { Vault } from "./models/vault"
|
||||||
export type { Item } from "./models/item"
|
export type { Item } from "./models/item"
|
||||||
@ -9,6 +8,8 @@ export type { Attachment, AttachmentMetadata } from "./models/attachment"
|
|||||||
export type { ItemField, ItemSection } from "./types"
|
export type { ItemField, ItemSection } from "./types"
|
||||||
export { Category, FieldType } from "./models"
|
export { Category, FieldType } from "./models"
|
||||||
|
|
||||||
|
export type { Adapter as IAdapter } from "./adapter/index"
|
||||||
|
|
||||||
interface IOptions {
|
interface IOptions {
|
||||||
/**
|
/**
|
||||||
* Path to `.opvault` directory
|
* Path to `.opvault` directory
|
||||||
@ -18,22 +19,33 @@ interface IOptions {
|
|||||||
/**
|
/**
|
||||||
* Adapter used to interact with the file system and cryptography modules
|
* Adapter used to interact with the file system and cryptography modules
|
||||||
*/
|
*/
|
||||||
adapter?: IAdapter | Promise<IAdapter>
|
adapter: Adapter | Promise<Adapter>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OnePassword instance
|
* OnePassword instance
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { OnePassword } from "opvault.js"
|
||||||
|
* import { adapter } from "opvault.js/src/adapter/node"
|
||||||
|
*
|
||||||
|
* const op = new OnePassword({
|
||||||
|
* path: "/path/to/1password/vault",
|
||||||
|
* adapter,
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* const profileNames = await op.getProfileNames()
|
||||||
|
* const vault = await op.getProfile(profileNames[0])
|
||||||
|
* const item = await vault.getItemByTitle("My Login")
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export class OnePassword {
|
export class OnePassword {
|
||||||
readonly #path: string
|
readonly #path: string
|
||||||
readonly #adapter: IAdapter | Promise<IAdapter>
|
readonly #adapter: Adapter | Promise<Adapter>
|
||||||
|
|
||||||
constructor({
|
constructor(options: IOptions) {
|
||||||
path,
|
this.#adapter = options.adapter
|
||||||
adapter = process.browser ? null! : require("./adapter").nodeAdapter,
|
this.#path = options.path
|
||||||
}: IOptions) {
|
|
||||||
this.#adapter = adapter
|
|
||||||
this.#path = path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,17 +53,15 @@ export class OnePassword {
|
|||||||
*/
|
*/
|
||||||
async getProfileNames() {
|
async getProfileNames() {
|
||||||
const { fs } = await this.#adapter
|
const { fs } = await this.#adapter
|
||||||
const children = await fs.readdir(this.#path)
|
|
||||||
const profiles: string[] = []
|
const profiles: string[] = []
|
||||||
await asyncMap(children, async child => {
|
|
||||||
const fullPath = resolve(this.#path, child)
|
for await (const { name, isDirectory } of fs.readDir(this.#path)) {
|
||||||
if (
|
const fullPath = resolve(this.#path, name)
|
||||||
(await fs.isDirectory(fullPath)) &&
|
if (isDirectory && (await fs.exists(resolve(fullPath, "profile.js")))) {
|
||||||
(await fs.exists(resolve(fullPath, "profile.js")))
|
profiles.push(name)
|
||||||
) {
|
|
||||||
profiles.push(child)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return profiles
|
return profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Buffer } from "buffer"
|
|
||||||
import type { Crypto } from "../crypto"
|
import type { Crypto } from "../crypto"
|
||||||
import { invariant } from "../errors"
|
import { invariant } from "../errors"
|
||||||
|
import { fromBase64, readIntLE, utf8Slice } from "../buffer"
|
||||||
|
|
||||||
type integer = number
|
type integer = number
|
||||||
|
|
||||||
@ -21,22 +21,22 @@ export interface AttachmentMetadata {
|
|||||||
export class Attachment {
|
export class Attachment {
|
||||||
#k: string
|
#k: string
|
||||||
#crypto: Crypto
|
#crypto: Crypto
|
||||||
#buffer: Buffer
|
#buffer: Uint8Array
|
||||||
|
|
||||||
#icon?: Buffer // png buffer
|
#icon?: Uint8Array // png buffer
|
||||||
#file?: Buffer
|
#file?: Uint8Array
|
||||||
#metadata?: AttachmentMetadata
|
#metadata?: AttachmentMetadata
|
||||||
|
|
||||||
private metadataSize: number
|
private metadataSize: number
|
||||||
private iconSize: number
|
private iconSize: number
|
||||||
|
|
||||||
constructor(crypto: Crypto, k: string, buffer: Buffer) {
|
constructor(crypto: Crypto, k: string, buffer: Uint8Array) {
|
||||||
this.#buffer = buffer
|
this.#buffer = buffer
|
||||||
this.#validate()
|
this.#validate()
|
||||||
this.#crypto = crypto
|
this.#crypto = crypto
|
||||||
this.#k = k
|
this.#k = k
|
||||||
this.metadataSize = buffer.readIntLE(8, 2)
|
this.metadataSize = readIntLE(buffer, 8, 2)
|
||||||
this.iconSize = buffer.readIntLE(12, 3)
|
this.iconSize = readIntLE(buffer, 12, 3)
|
||||||
|
|
||||||
crypto.onLock(() => {
|
crypto.onLock(() => {
|
||||||
this.#lock()
|
this.#lock()
|
||||||
@ -49,13 +49,13 @@ export class Attachment {
|
|||||||
#validate() {
|
#validate() {
|
||||||
const file = this.#buffer
|
const file = this.#buffer
|
||||||
invariant(
|
invariant(
|
||||||
file.slice(0, 6).toString("utf-8") === "OPCLDA",
|
utf8Slice(file.slice(0, 6)) === "OPCLDA",
|
||||||
"Attachment must start with OPCLDA"
|
"Attachment must start with OPCLDA"
|
||||||
)
|
)
|
||||||
// @TODO: Re-enable this
|
// @TODO: Re-enable this
|
||||||
false &&
|
false &&
|
||||||
invariant(
|
invariant(
|
||||||
file.readIntLE(7, 1) === 1,
|
readIntLE(file, 7, 1) === 1,
|
||||||
"The version for this attachment file format is not supported."
|
"The version for this attachment file format is not supported."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -86,14 +86,11 @@ export class Attachment {
|
|||||||
cipher
|
cipher
|
||||||
)
|
)
|
||||||
|
|
||||||
const metadata = JSON.parse(buffer.slice(16, 16 + metadataSize).toString("utf-8"))
|
const metadata = JSON.parse(utf8Slice(buffer.slice(16, 16 + metadataSize)))
|
||||||
metadata.overview = JSON.parse(
|
metadata.overview = JSON.parse(
|
||||||
(
|
utf8Slice(
|
||||||
await crypto.decryptOPData(
|
await crypto.decryptOPData(fromBase64(metadata.overview), crypto.overview)
|
||||||
Buffer.from(metadata.overview, "base64"),
|
)
|
||||||
crypto.overview
|
|
||||||
)
|
|
||||||
).toString()
|
|
||||||
)
|
)
|
||||||
this.#metadata = metadata
|
this.#metadata = metadata
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ export class Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
addAttachment(buffer: Buffer) {
|
addAttachment(buffer: Uint8Array) {
|
||||||
this.attachments.push(new Attachment(this.#crypto, this.#data.k, buffer))
|
this.attachments.push(new Attachment(this.#crypto, this.#data.k, buffer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { IAdapter } from "../adapter"
|
import type { Adapter } from "../adapter"
|
||||||
import { HMACAssertionError, invariant } from "../errors"
|
import { HMACAssertionError, OPVaultError, invariant } from "../errors"
|
||||||
import { OnePasswordFileManager } from "../fs"
|
import { OnePasswordFileManager } from "../fs"
|
||||||
import { i18n } from "../i18n"
|
|
||||||
import type { EncryptedItem } from "./item"
|
import type { EncryptedItem } from "./item"
|
||||||
import { Crypto } from "../crypto"
|
import { Crypto } from "../crypto"
|
||||||
import { Item } from "./item"
|
import { Item } from "./item"
|
||||||
@ -21,7 +20,7 @@ export class Vault {
|
|||||||
#itemsMap = new WeakValueMap<string, Item>()
|
#itemsMap = new WeakValueMap<string, Item>()
|
||||||
#crypto: Crypto
|
#crypto: Crypto
|
||||||
|
|
||||||
readonly onLock = createEventEmitter<void>()
|
readonly #onLock = createEventEmitter<void>()
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
@ -41,8 +40,8 @@ export class Vault {
|
|||||||
* Create a new OnePassword Vault instance and read all bands.
|
* Create a new OnePassword Vault instance and read all bands.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
static async of(path: string, profileName = "default", adapter: IAdapter) {
|
static async of(path: string, profileName = "default", adapter: Adapter) {
|
||||||
const crypto = new Crypto(i18n, adapter)
|
const crypto = new Crypto(adapter)
|
||||||
const files = await OnePasswordFileManager(adapter.fs, path, profileName)
|
const files = await OnePasswordFileManager(adapter.fs, path, profileName)
|
||||||
const profile = JSON.parse(
|
const profile = JSON.parse(
|
||||||
stripText(await files.getProfile(), /^var profile\s*=/, ";")
|
stripText(await files.getProfile(), /^var profile\s*=/, ";")
|
||||||
@ -66,8 +65,7 @@ export class Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = await files.getAttachments()
|
for await (const att of files.getAttachments()) {
|
||||||
for (const att of attachments) {
|
|
||||||
const file = itemsMap.get(att.itemUUID)
|
const file = itemsMap.get(att.itemUUID)
|
||||||
invariant(file, `Item ${att.itemUUID} of attachment does not exist`)
|
invariant(file, `Item ${att.itemUUID} of attachment does not exist`)
|
||||||
file.addAttachment(await att.getFile())
|
file.addAttachment(await att.getFile())
|
||||||
@ -76,6 +74,9 @@ export class Vault {
|
|||||||
return new Vault(profile, bands, crypto, itemsMap)
|
return new Vault(profile, bands, crypto, itemsMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the overview of an item given the `uuid`.
|
||||||
|
*/
|
||||||
getOverview(uuid: string) {
|
getOverview(uuid: string) {
|
||||||
this.#crypto.assertUnlocked()
|
this.#crypto.assertUnlocked()
|
||||||
return this.#items.find(x => x.uuid === uuid)?.overview
|
return this.#items.find(x => x.uuid === uuid)?.overview
|
||||||
@ -102,7 +103,7 @@ export class Vault {
|
|||||||
await this.#crypto.unlock(this.#profile, masterPassword)
|
await this.#crypto.unlock(this.#profile, masterPassword)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof HMACAssertionError) {
|
if (e instanceof HMACAssertionError) {
|
||||||
throw new Error(i18n.error.invalidPassword)
|
throw new OPVaultError("Invalid password", "INVALID_PASSWORD")
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@ -114,7 +115,7 @@ export class Vault {
|
|||||||
*/
|
*/
|
||||||
lock() {
|
lock() {
|
||||||
this.#crypto.lock()
|
this.#crypto.lock()
|
||||||
this.onLock()
|
this.#onLock()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +123,14 @@ export class Vault {
|
|||||||
return this.#crypto.locked
|
return this.#crypto.locked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the item with the given `uuid`
|
||||||
|
*/
|
||||||
getItem(uuid: string): Promise<Item | undefined>
|
getItem(uuid: string): Promise<Item | undefined>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first item with the given title
|
||||||
|
*/
|
||||||
getItem(filter: { title: string }): Promise<Item | undefined>
|
getItem(filter: { title: string }): Promise<Item | undefined>
|
||||||
|
|
||||||
async getItem(filter: any) {
|
async getItem(filter: any) {
|
||||||
|
9
packages/web/src/electron/ipc-types.d.ts
vendored
9
packages/web/src/electron/ipc-types.d.ts
vendored
@ -1,9 +1,10 @@
|
|||||||
|
import type { DirEntry } from "opvault.js/src/adapter"
|
||||||
|
|
||||||
export interface IPC {
|
export interface IPC {
|
||||||
showDirectoryPicker(): Promise<string | undefined>
|
showDirectoryPicker(): Promise<string | undefined>
|
||||||
pathExists(path: string): Promise<boolean>
|
pathExists(path: string): Promise<boolean>
|
||||||
readdir(path: string): Promise<string[]>
|
readDir(path: string): Promise<DirEntry[]>
|
||||||
readBuffer(path: string): Promise<Uint8Array>
|
readFile(path: string): Promise<Uint8Array>
|
||||||
readFile(path: string): Promise<string>
|
readTextFile(path: string): Promise<string>
|
||||||
writeFile(path: string, data: string): Promise<void>
|
writeFile(path: string, data: string): Promise<void>
|
||||||
isDirectory(path: string): Promise<boolean>
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import fs, { promises } from "fs"
|
import fs, { promises } from "fs"
|
||||||
import { ipcMain, dialog } from "electron"
|
import { ipcMain, dialog } from "electron"
|
||||||
|
import { adapter } from "opvault.js/src/adapter/node"
|
||||||
|
import type { DirEntry } from "opvault.js/src/adapter"
|
||||||
import type { IPC } from "./ipc-types"
|
import type { IPC } from "./ipc-types"
|
||||||
|
|
||||||
registerService({
|
registerService({
|
||||||
@ -15,11 +17,11 @@ registerService({
|
|||||||
return fs.existsSync(path)
|
return fs.existsSync(path)
|
||||||
},
|
},
|
||||||
|
|
||||||
async readBuffer(_, path) {
|
async readFile(_, path) {
|
||||||
return promises.readFile(path)
|
return promises.readFile(path)
|
||||||
},
|
},
|
||||||
|
|
||||||
async readFile(_, path) {
|
async readTextFile(_, path) {
|
||||||
return promises.readFile(path, "utf-8")
|
return promises.readFile(path, "utf-8")
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -27,13 +29,12 @@ registerService({
|
|||||||
await promises.writeFile(path, content)
|
await promises.writeFile(path, content)
|
||||||
},
|
},
|
||||||
|
|
||||||
async readdir(_, path) {
|
async readDir(_, path) {
|
||||||
return promises.readdir(path)
|
const entries: DirEntry[] = []
|
||||||
},
|
for await (const dirent of adapter.fs.readDir(path)) {
|
||||||
|
entries.push(dirent)
|
||||||
async isDirectory(_, path) {
|
}
|
||||||
const stats = await promises.stat(path)
|
return entries
|
||||||
return stats.isDirectory()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Buffer } from "buffer"
|
import type { Adapter } from "opvault.js/src/adapter"
|
||||||
import type { IAdapter } from "opvault.js/src/adapters"
|
|
||||||
import type { IPC } from "../electron/ipc-types"
|
import type { IPC } from "../electron/ipc-types"
|
||||||
import { memoize } from "./memoize"
|
import { memoize } from "./memoize"
|
||||||
|
|
||||||
@ -9,14 +8,15 @@ export async function openDirectory() {
|
|||||||
return ipc.showDirectoryPicker()
|
return ipc.showDirectoryPicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const electronAdapter: IAdapter = {
|
export const electronAdapter: Adapter = {
|
||||||
fs: {
|
fs: {
|
||||||
exists: path => ipc.pathExists(path),
|
exists: path => ipc.pathExists(path),
|
||||||
readBuffer: path => ipc.readBuffer(path).then(Buffer.from),
|
|
||||||
readFile: path => ipc.readFile(path),
|
readFile: path => ipc.readFile(path),
|
||||||
readdir: path => ipc.readdir(path),
|
readTextFile: path => ipc.readTextFile(path),
|
||||||
writeFile: (path, data) => ipc.writeFile(path, data),
|
async *readDir(path) {
|
||||||
isDirectory: path => ipc.isDirectory(path),
|
yield* await ipc.readDir(path)
|
||||||
|
},
|
||||||
|
writeTextFile: (path, data) => ipc.writeFile(path, data),
|
||||||
},
|
},
|
||||||
subtle: crypto.subtle,
|
subtle: crypto.subtle,
|
||||||
}
|
}
|
||||||
|
3416
pnpm-lock.yaml
generated
3416
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,104 +0,0 @@
|
|||||||
import { resolve } from "path";
|
|
||||||
import { describe, it, beforeEach } from "mocha";
|
|
||||||
import { expect } from "chai";
|
|
||||||
|
|
||||||
import type { Vault } from "../packages/opvault.js/src/index";
|
|
||||||
import { OnePassword } from "../packages/opvault.js/src/index";
|
|
||||||
|
|
||||||
describe("OnePassword", () => {
|
|
||||||
const freddy = resolve(__dirname, "../freddy-2013-12-04.opvault");
|
|
||||||
|
|
||||||
describe("getProfileNames", () => {
|
|
||||||
it("freddy", async () => {
|
|
||||||
const instance = new OnePassword({ path: freddy });
|
|
||||||
expect(await instance.getProfileNames()).to.deep.equal(["default"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip("ignores faulty folders", async () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("unlock", () => {
|
|
||||||
let vault: Vault;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
vault = await new OnePassword({ path: freddy }).getProfile("default");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts correct password", async () => {
|
|
||||||
await expect(vault.unlock("freddy")).to.be.fulfilled;
|
|
||||||
expect(vault.isLocked).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects wrong password", () => {
|
|
||||||
["Freddy", "_freddy", ""].forEach(async (password) => {
|
|
||||||
await expect(vault.unlock(password)).to.be.rejectedWith(
|
|
||||||
"Invalid password"
|
|
||||||
);
|
|
||||||
expect(vault.isLocked).to.be.true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("content", () => {
|
|
||||||
let vault: Vault;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
vault = await new OnePassword({ path: freddy }).getProfile("default");
|
|
||||||
await vault.unlock("freddy");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reads notes", async () => {
|
|
||||||
const item = (await vault.getItem({
|
|
||||||
title: "A note with some attachments",
|
|
||||||
}))!;
|
|
||||||
expect(item).to.exist;
|
|
||||||
expect(item.uuid).to.equal("F2DB5DA3FCA64372A751E0E85C67A538");
|
|
||||||
expect(item.attachments).to.have.lengthOf(2);
|
|
||||||
expect(item.details).to.deep.equal({
|
|
||||||
notesPlain: "This note has two attachments.",
|
|
||||||
});
|
|
||||||
expect(item.overview).to.deep.equal({
|
|
||||||
title: "A note with some attachments",
|
|
||||||
ps: 0,
|
|
||||||
ainfo: "This note has two attachments.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("decrypts items", async () => {
|
|
||||||
const decrypted = require("./decrypted.json");
|
|
||||||
expect(vault.isLocked).to.be.false;
|
|
||||||
for (const [uuid, item] of Object.entries<any>(decrypted)) {
|
|
||||||
const actual = await vault.getItem(uuid);
|
|
||||||
expect(actual).to.exist;
|
|
||||||
expect(actual!.overview).to.deep.equal(item.overview);
|
|
||||||
expect(actual!.details).to.deep.equal(item.itemDetails);
|
|
||||||
expect(actual!.attachments).to.have.lengthOf(item.attachments.length);
|
|
||||||
for (const [i, attachment] of actual!.attachments.entries()) {
|
|
||||||
const expected = item.attachments[i];
|
|
||||||
await attachment.unlock();
|
|
||||||
expect(attachment.metadata).to.deep.equal(expected.metadata);
|
|
||||||
expect(attachment.file.toString("base64")).to.deep.equal(
|
|
||||||
expected.file
|
|
||||||
);
|
|
||||||
expect(attachment.icon.toString("base64")).to.deep.equal(
|
|
||||||
expected.icon
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("lock", () => {
|
|
||||||
it("locks", async () => {
|
|
||||||
const instance = new OnePassword({ path: freddy });
|
|
||||||
const vault = await instance.getProfile("default");
|
|
||||||
await vault.unlock("freddy");
|
|
||||||
expect(vault.isLocked).to.be.false;
|
|
||||||
|
|
||||||
vault.lock();
|
|
||||||
expect(vault.isLocked).to.be.true;
|
|
||||||
expect(vault.getItem("F2DB5DA3FCA64372A751E0E85C67A538")).to.eventually
|
|
||||||
.throw;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,43 +0,0 @@
|
|||||||
import { describe, it } from "mocha";
|
|
||||||
import { expect } from "chai";
|
|
||||||
|
|
||||||
import { WeakValueMap } from "../packages/opvault.js/src/weakMap";
|
|
||||||
|
|
||||||
declare const gc: () => void;
|
|
||||||
|
|
||||||
describe("WeakValueMap", () => {
|
|
||||||
interface Value {
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
it("covers base use cases", () => {
|
|
||||||
const map = new WeakValueMap<string, Value>();
|
|
||||||
const object = { value: 1 };
|
|
||||||
map.set("key", object);
|
|
||||||
expect(map.get("key")!.value).to.equal(1);
|
|
||||||
expect(map.delete("key")).to.be.true;
|
|
||||||
expect(!map.delete("key")).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("overrides previous value", () => {
|
|
||||||
const map = new WeakValueMap<string, Value>();
|
|
||||||
map.set("key", { value: 2 });
|
|
||||||
map.set("key", { value: 3 });
|
|
||||||
expect(map.get("key")!.value).to.equal(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("deletes garbage collected values", (done) => {
|
|
||||||
const map = new WeakValueMap<string, Value>();
|
|
||||||
map.set("key", { value: 1 });
|
|
||||||
setTimeout(() => {
|
|
||||||
gc();
|
|
||||||
expect(map.has("key")).to.be.false;
|
|
||||||
map.set("key", { value: 2 });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
gc();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user