feat(sync): rewrite struct parsing

This commit is contained in:
Simon Chan 2020-09-22 02:00:28 +08:00
parent 4da02d5cdc
commit 5006771c01
6 changed files with 832 additions and 401 deletions

View file

@ -12,6 +12,14 @@ const PrivateKeyStorageKey = 'private-key';
const Utf8Encoder = new TextEncoder();
const Utf8Decoder = new TextDecoder();
export function encodeUtf8(input: string): ArrayBuffer {
return Utf8Encoder.encode(input);
}
export function decodeUtf8(buffer: ArrayBuffer): string {
return Utf8Decoder.decode(buffer);
}
export default class AdbWebBackend implements AdbBackend {
public static async fromDevice(device: USBDevice): Promise<AdbWebBackend> {
await device.open();
@ -117,11 +125,11 @@ export default class AdbWebBackend implements AdbBackend {
}
public encodeUtf8(input: string): ArrayBuffer {
return Utf8Encoder.encode(input);
return encodeUtf8(input);
}
public decodeUtf8(buffer: ArrayBuffer): string {
return Utf8Decoder.decode(buffer);
return decodeUtf8(buffer);
}
public async write(buffer: ArrayBuffer): Promise<void> {

View file

@ -1,7 +1,7 @@
import { AutoDisposable } from '@yume-chan/event';
import { AdbBufferedStream } from './buffered-stream';
import { AdbStream } from './stream';
import { AutoResetEvent, Struct, StructInitType, StructReader } from './utils';
import { AutoResetEvent, Struct, StructInitType, StructValueType } from './utils';
export enum AdbSyncRequestId {
List = 'LIST',
@ -31,9 +31,10 @@ export type AdbSyncStringRequestId =
AdbSyncRequestId.Receive;
const AdbSyncStringRequest =
new Struct(true)
.fixedLengthString('id', 4, undefined as any as AdbSyncStringRequestId)
.lengthPrefixedString('value', 'int32');
new Struct({ littleEndian: true })
.string('id', { length: 4 })
.uint32('valueLength')
.string('value', { lengthField: 'valueLength' });
// https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36
export enum LinuxFileType {
@ -42,77 +43,38 @@ export enum LinuxFileType {
Link = 0o12,
}
const AdbSyncStatResponseStruct =
new Struct(true)
export const AdbSyncStatResponse =
new Struct({ littleEndian: true })
.int32('mode')
.int32('size')
.int32('lastModifiedTime')
export class AdbSyncStatResponse {
public static readonly size = AdbSyncStatResponseStruct.size;
public static async parse(reader: StructReader): Promise<AdbSyncStatResponse> {
const struct = await AdbSyncStatResponseStruct.parse(reader);
if (struct.mode === 0 && struct.size === 0 && struct.lastModifiedTime === 0) {
.extra({
id: AdbSyncResponseId.Stat as const,
get type() { return this.mode >> 12 as LinuxFileType; },
get permission() { return this.mode & 0b00001111_11111111; },
})
.afterParsed((object) => {
if (object.mode === 0 &&
object.size === 0 &&
object.lastModifiedTime === 0
) {
throw new Error('lstat failed');
}
return new AdbSyncStatResponse(struct.mode, struct.size, struct.lastModifiedTime);
}
});
public readonly id = AdbSyncResponseId.Stat;
export const AdbSyncEntryResponse =
AdbSyncStatResponse
.afterParsed()
.uint32('nameLength')
.string('name', { lengthField: 'nameLength' })
.extra({ id: AdbSyncResponseId.Entry as const });
public readonly type: LinuxFileType;
export type AdbSyncEntryResponse = StructValueType<typeof AdbSyncEntryResponse>;
public readonly mode: number;
public readonly size: number;
public readonly lastModifiedTime: number;
public constructor(mode: number, size: number, lastModifiedTime: number) {
this.type = mode >> 12 as LinuxFileType;
this.mode = mode & 0b00001111_11111111;
this.size = size;
this.lastModifiedTime = lastModifiedTime;
}
}
const AdbSyncEntryResponseStruct =
AdbSyncStatResponseStruct
.lengthPrefixedString('name', 'int32');
export class AdbSyncEntryResponse {
public static readonly size = AdbSyncEntryResponseStruct.size;
public static async parse(reader: StructReader): Promise<AdbSyncEntryResponse> {
const struct = await AdbSyncEntryResponseStruct.parse(reader);
return new AdbSyncEntryResponse(struct.mode, struct.size, struct.lastModifiedTime, struct.name);
}
public readonly id = AdbSyncResponseId.Entry;
public readonly type: LinuxFileType;
public readonly mode: number;
public readonly size: number;
public readonly lastModifiedTime: number;
public readonly name: string;
public constructor(mode: number, size: number, lastModifiedTime: number, name: string) {
this.type = mode >> 12 as LinuxFileType;
this.mode = mode & 0b00001111_11111111;
this.size = size;
this.lastModifiedTime = lastModifiedTime;
this.name = name;
}
}
const AdbSyncDataResponse =
new Struct(true)
.lengthPrefixedBuffer('data', 'int32')
export const AdbSyncDataResponse =
new Struct({ littleEndian: true })
.uint32('dataLength')
.arrayBuffer('data', { lengthField: 'dataLength' })
.extra({ id: AdbSyncResponseId.Data } as const);
export class AdbSyncDoneResponse {
@ -121,16 +83,13 @@ export class AdbSyncDoneResponse {
public readonly id = AdbSyncResponseId.Done;
}
const AdbSyncFailResponseStruct =
new Struct(true)
.lengthPrefixedString('message', 'int32');
class AdbSyncFailResponse {
public static async parse(reader: StructReader): Promise<never> {
const struct = await AdbSyncFailResponseStruct.parse(reader);
throw new Error(struct.message);
}
}
export const AdbSyncFailResponse =
new Struct({ littleEndian: true })
.uint32('messageLength')
.string('message', { lengthField: 'messageLength' })
.afterParsed(object => {
throw new Error(object.message);
});
async function parseResponse(stream: AdbBufferedStream, size: number) {
// DONE responses' size are always same as the request's normal response.
@ -141,6 +100,7 @@ async function parseResponse(stream: AdbBufferedStream, size: number) {
const structReader = {
read: stream.read.bind(stream),
decodeUtf8: stream.backend.decodeUtf8.bind(stream.backend),
encodeUtf8: stream.backend.encodeUtf8.bind(stream.backend),
};
switch (id) {
case AdbSyncResponseId.Entry:
@ -209,7 +169,7 @@ export class AdbSync extends AutoDisposable {
}
public async list(path: string) {
const results: AdbSyncEntryResponse[] = [];
const results: StructValueType<typeof AdbSyncEntryResponse>[] = [];
for await (const entry of this.iterate(path)) {
results.push(entry);
}
@ -225,7 +185,7 @@ export class AdbSync extends AutoDisposable {
const response = await parseResponse(this.stream, AdbSyncDataResponse.size);
switch (response.id) {
case AdbSyncResponseId.Data:
yield response.data;
yield response.data!;
break;
case AdbSyncResponseId.Done:
return;

View file

@ -1,259 +1,792 @@
const enum StructFieldType {
Int32,
FixedLengthString,
LengthPrefixedBuffer,
}
const BackingField = Symbol('BackingField');
interface StructFieldBase {
type: StructFieldType;
export namespace StructField {
export const enum Type {
Number,
FixedLengthArray,
VariableLengthArray,
}
export interface BaseOptions {
}
export interface Base<TOptions extends BaseOptions = BaseOptions> {
type: Type;
name: PropertyKey;
}
interface StructInt32Field extends StructFieldBase {
type: StructFieldType.Int32;
options: TOptions;
}
signed: boolean;
}
export type Parser<TField extends Any> = (options: {
field: TField;
object: any;
options: StructOptions;
reader: StructReader;
}) => Promise<void>;
interface StructFixedLengthStringField extends StructFieldBase {
type: StructFieldType.FixedLengthString;
export type Initializer<TField extends Any> = (options: {
field: TField;
init: any;
object: any;
options: StructOptions;
writer: StructWriter;
}) => void;
export type Writer<TField extends Any> = (options: {
dataView: DataView;
field: TField;
object: any;
offset: number;
options: StructOptions;
writer: StructWriter;
}) => void;
export interface Methods<TField extends Any> {
type: Type;
getLength(options: {
field: TField;
options: StructOptions;
}): number;
parse: Parser<TField>;
initialize?: Initializer<TField>;
getVariableLength?(options: {
field: TField,
object: any,
options: StructOptions,
writer: StructWriter,
}): number;
write: Writer<TField>;
}
const registry: Record<number, Methods<any>> = {};
export function getType(type: Type): Methods<Any> {
return registry[type as number];
}
export function registerType<TField extends Any, TMethods extends Methods<TField>>(
_field: TField,
methods: TMethods
): void {
registry[methods.type as number] = methods;
}
export namespace Number {
export type TypeScriptType = number;
export const enum SubType {
Int32,
Uint32,
}
export const SizeMap: Record<SubType, number> = {
[SubType.Int32]: 4,
[SubType.Uint32]: 4,
};
export const DataViewGetterMap = {
[SubType.Int32]: 'getInt32',
[SubType.Uint32]: 'getUint32',
} as const;
export const DataViewSetterMap = {
[SubType.Int32]: 'setInt32',
[SubType.Uint32]: 'setUint32',
} as const;
}
registerType(undefined as unknown as Number, {
type: Type.Number,
getLength({ field }) {
return Number.SizeMap[field.subType];
},
async parse({ field, object, options, reader }) {
const buffer = await reader.read(Number.SizeMap[field.subType]);
const view = new DataView(buffer);
object[field.name] = view[Number.DataViewGetterMap[field.subType]](
0,
options.littleEndian
);
},
write({ dataView, field, object, offset, options }) {
dataView[Number.DataViewSetterMap[field.subType]](
offset,
object[field.name],
options.littleEndian
);
},
});
export interface Number<TOptions extends BaseOptions = BaseOptions> extends Base<TOptions> {
type: Type.Number;
subType: Number.SubType;
}
export namespace Array {
export const enum SubType {
ArrayBuffer,
String,
}
export type TypeScriptType<TType extends SubType = SubType> =
TType extends SubType.ArrayBuffer ? ArrayBuffer :
TType extends SubType.String ? string :
ArrayBuffer | string;
export interface BackingField {
buffer?: ArrayBuffer;
string?: string;
}
export function getBackingField(object: any, name: PropertyKey): BackingField {
return object[BackingField][name];
}
export function setBackingField(object: any, name: PropertyKey, value: BackingField): void {
object[BackingField][name] = value;
}
export function initialize(object: any, field: Array, value: BackingField): void {
switch (field.subType) {
case StructField.Array.SubType.ArrayBuffer:
Object.defineProperty(object, field.name, {
configurable: true,
enumerable: true,
get(): ArrayBuffer {
return getBackingField(object, field.name).buffer!;
},
set(buffer: ArrayBuffer) {
setBackingField(object, field.name, { buffer });
},
});
break;
case StructField.Array.SubType.String:
Object.defineProperty(object, field.name, {
configurable: true,
enumerable: true,
get(): string {
return getBackingField(object, field.name).string!;
},
set(string: string) {
setBackingField(object, field.name, { string });
},
});
break;
default:
throw new Error('Unknown type');
}
setBackingField(object, field.name, value);
}
}
export interface Array<
TType extends Array.SubType = Array.SubType,
TOptions extends BaseOptions = BaseOptions
> extends Base<TOptions> {
subType: TType;
}
export namespace FixedLengthArray {
export interface Options extends BaseOptions {
length: number;
}
}
}
interface StructLengthPrefixedBufferField extends StructFieldBase {
type: StructFieldType.LengthPrefixedBuffer;
registerType(undefined as unknown as FixedLengthArray, {
type: Type.FixedLengthArray,
lengthType: 'int32';
getLength({ field }) {
return field.options.length;
},
subType: 'buffer' | 'string';
}
async parse({ field, object, reader }) {
const value: Array.BackingField = {
buffer: await reader.read(field.options.length),
};
type StructField =
StructInt32Field |
StructFixedLengthStringField |
StructLengthPrefixedBufferField;
switch (field.subType) {
case Array.SubType.ArrayBuffer:
break;
case Array.SubType.String:
value.string = reader.decodeUtf8(value.buffer!);
break;
default:
throw new Error('Unknown type');
}
export interface StructReader {
decodeUtf8(buffer: ArrayBuffer): string;
Array.initialize(object, field, value);
},
read(length: number): Promise<ArrayBuffer>;
initialize({ field, init, object }) {
Array.initialize(object, field, {});
object[field.name] = init[field.name];
},
write({ dataView, field, object, offset, writer }) {
const backingField = Array.getBackingField(object, field.name);
backingField.buffer ??=
writer.encodeUtf8(backingField.string!);
new Uint8Array(dataView.buffer).set(
new Uint8Array(backingField.buffer),
offset
);
}
});
export interface FixedLengthArray<
TType extends Array.SubType = Array.SubType,
TOptions extends FixedLengthArray.Options = FixedLengthArray.Options
> extends Array<TType, TOptions> {
type: Type.FixedLengthArray;
options: TOptions;
}
export namespace VariableLengthArray {
export type TypeScriptTypeCanBeUndefined<
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
> =
TEmptyBehavior extends EmptyBehavior.Empty ? never :
undefined;
export type TypeScriptType<
TType extends Array.SubType = Array.SubType,
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
> =
Array.TypeScriptType<TType> |
TypeScriptTypeCanBeUndefined<TEmptyBehavior>;
export const enum EmptyBehavior {
Undefined,
Empty,
}
export type KeyOfType<TObject, TProperty> =
{
[TKey in keyof TObject]:
TObject[TKey] extends TProperty ? TKey : never
}[keyof TObject];
export interface Options<
TObject = object,
TLengthField extends KeyOfType<TObject, number> = any,
TEmptyBehavior extends EmptyBehavior = EmptyBehavior
> extends BaseOptions {
lengthField: TLengthField;
emptyBehavior?: TEmptyBehavior;
}
export function getLengthBackingField(object: any, field: VariableLengthArray): number | undefined {
return object[BackingField][field.options.lengthField];
}
export function setLengthBackingField(
object: any,
field: VariableLengthArray,
value: number | undefined
) {
object[BackingField][field.options.lengthField] = value;
}
export function initialize(
object: any,
field: VariableLengthArray,
value: Array.BackingField,
writer: StructWriter,
): void {
Array.initialize(object, field, value);
const descriptor = Object.getOwnPropertyDescriptor(object, field.name)!;
delete object[field.name];
switch (field.subType) {
case Array.SubType.ArrayBuffer:
Object.defineProperty(object, field.name, {
...descriptor,
set(buffer: ArrayBuffer | undefined) {
descriptor.set!.call(object, buffer);
setLengthBackingField(object, field, buffer?.byteLength ?? 0);
},
});
delete object[field.options.lengthField];
Object.defineProperty(object, field.options.lengthField, {
configurable: true,
enumerable: true,
get() {
return getLengthBackingField(object, field);
}
});
break;
case Array.SubType.String:
Object.defineProperty(object, field.name, {
...descriptor,
set(string: string | undefined) {
descriptor.set!.call(object, string);
if (string) {
setLengthBackingField(object, field, undefined);
} else {
setLengthBackingField(object, field, 0);
}
},
});
delete object[field.options.lengthField];
Object.defineProperty(object, field.options.lengthField, {
configurable: true,
enumerable: true,
get() {
let value = getLengthBackingField(object, field);
if (value === undefined) {
const backingField = Array.getBackingField(object, field.name);
const buffer = writer.encodeUtf8(backingField.string!);
backingField.buffer = buffer;
value = buffer.byteLength;
setLengthBackingField(object, field, value);
}
return value;
}
});
break;
default:
throw new Error('Unknown type');
}
Array.setBackingField(object, field.name, value);
if (value.buffer) {
setLengthBackingField(object, field, value.buffer.byteLength);
}
}
}
registerType(undefined as unknown as VariableLengthArray, {
type: Type.VariableLengthArray,
getLength() { return 0; },
async parse({ field, object, reader }) {
const value: Array.BackingField = {};
const length = object[field.options.lengthField];
if (length === 0) {
if (field.options.emptyBehavior === VariableLengthArray.EmptyBehavior.Empty) {
value.buffer = new ArrayBuffer(0);
value.string = '';
}
VariableLengthArray.initialize(object, field, value, reader);
return;
}
value.buffer = await reader.read(length);
switch (field.subType) {
case Array.SubType.ArrayBuffer:
break;
case Array.SubType.String:
value.string = reader.decodeUtf8(value.buffer);
break;
default:
throw new Error('Unknown type');
}
VariableLengthArray.initialize(object, field, value, reader);
},
initialize({ field, init, object, writer }) {
VariableLengthArray.initialize(object, field, {}, writer);
object[field.name] = init[field.name];
},
getVariableLength({ field, object }) {
return object[field.options.lengthField];
},
write({ dataView, field, object, offset }) {
const backingField = Array.getBackingField(object, field.name);
new Uint8Array(dataView.buffer).set(
new Uint8Array(backingField.buffer!),
offset
);
},
});
export interface VariableLengthArray<
TType extends Array.SubType = Array.SubType,
TObject = object,
TLengthField extends VariableLengthArray.KeyOfType<TObject, number> = any,
TEmptyBehavior extends VariableLengthArray.EmptyBehavior = VariableLengthArray.EmptyBehavior,
TOptions extends VariableLengthArray.Options<TObject, TLengthField, TEmptyBehavior> = VariableLengthArray.Options<TObject, TLengthField, TEmptyBehavior>
> extends Array<TType, TOptions> {
type: Type.VariableLengthArray;
options: TOptions;
}
export type Any =
Number |
FixedLengthArray |
VariableLengthArray;
}
export interface StructWriter {
encodeUtf8(input: string): ArrayBuffer;
}
type KeyOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
export interface StructReader extends StructWriter {
decodeUtf8(buffer: ArrayBuffer): string;
export type StructValueType<T extends Struct<unknown, unknown>> =
T extends { parse(reader: StructReader): Promise<infer R> } ? R : never;
read(length: number): Promise<ArrayBuffer>;
}
export type StructInitType<T extends Struct<unknown, unknown>> =
T extends { create(value: infer R): any } ? R : never;
export type StructValueType<T extends Struct<unknown, unknown, unknown>> =
T extends { parse(reader: StructReader): Promise<infer R>; } ? R : never;
export default class Struct<T, TInit> {
private littleEndian: boolean;
export type StructInitType<T extends Struct<unknown, unknown, unknown>> =
T extends { create(value: infer R, ...args: any): any; } ? R : never;
export interface StructOptions {
littleEndian: boolean;
}
export const StructDefaultOptions: Readonly<StructOptions> = {
littleEndian: false,
};
interface ArrayInitializer<TObject, TAfterParsed, TInit> {
<
TName extends PropertyKey,
TType extends StructField.Array.SubType,
TTypeScriptType = StructField.Array.TypeScriptType<TType>
>(
name: TName,
type: TType,
options: StructField.FixedLengthArray.Options,
typescriptType?: () => TTypeScriptType,
): Struct<
TObject & Record<TName, TTypeScriptType>,
TAfterParsed,
TInit & Record<TName, TTypeScriptType>
>;
<
TName extends PropertyKey,
TType extends StructField.Array.SubType,
TLengthField extends StructField.VariableLengthArray.KeyOfType<TInit, number>,
TEmptyBehavior extends StructField.VariableLengthArray.EmptyBehavior,
TTypeScriptType = StructField.VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
>(
name: TName,
type: TType,
options: StructField.VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
typescriptType?: () => TTypeScriptType,
): Struct<
TObject & Record<TName, TTypeScriptType>,
TAfterParsed,
Omit<TInit, TLengthField> & Record<TName, TTypeScriptType>
>;
}
interface ArrayTypeInitializer<
TObject,
TAfterParsed,
TInit,
TType extends StructField.Array.SubType
> {
<
TName extends PropertyKey,
TTypeScriptType = StructField.Array.TypeScriptType<TType>
>(
name: TName,
options: StructField.FixedLengthArray.Options,
typescriptType?: () => TTypeScriptType,
): Struct<
TObject & Record<TName, TTypeScriptType>,
TAfterParsed,
TInit & Record<TName, TTypeScriptType>
>;
<
TName extends PropertyKey,
TLengthField extends StructField.VariableLengthArray.KeyOfType<TInit, number>,
TEmptyBehavior extends StructField.VariableLengthArray.EmptyBehavior,
TTypeScriptType = StructField.VariableLengthArray.TypeScriptType<TType, TEmptyBehavior>
>(
name: TName,
options: StructField.VariableLengthArray.Options<TInit, TLengthField, TEmptyBehavior>,
_typescriptType?: () => TTypeScriptType,
): Struct<
TObject & Record<TName, TTypeScriptType>,
TAfterParsed,
Omit<TInit, TLengthField> & Record<TName, TTypeScriptType>
>;
}
export type StructAfterParsed<TObject, TResult> =
(this: TObject, object: TObject) => TResult;
export default class Struct<TObject = {}, TAfterParsed = undefined, TInit = {}> {
public readonly options: Readonly<StructOptions>;
private _size = 0;
public get size() { return this._size; }
private fields: StructField[] = [];
private fields: StructField.Any[] = [];
private _extra?: any;
private _extra: PropertyDescriptorMap = {};
public constructor(littleEndian = false) {
this.littleEndian = littleEndian;
private _afterParsed?: StructAfterParsed<any, any>;
public constructor(options: Partial<StructOptions> = StructDefaultOptions) {
this.options = { ...StructDefaultOptions, ...options };
}
private clone(): Struct<any, any> {
const result = new Struct<any, any>(this.littleEndian);
private clone(): Struct<any, any, any> {
const result = new Struct<any, any, any>(this.options);
result.fields = this.fields.slice();
result._size = this._size;
result._extra = this._extra;
result._afterParsed = this._afterParsed;
return result;
}
public int32<K extends PropertyKey>(name: K): Struct<
T & { [KK in K]: number },
T & { [KK in K]: number }
> {
const result = this.clone();
result.fields.push({ type: StructFieldType.Int32, name, signed: true });
result._size += 4;
return result;
}
public uint32<K extends PropertyKey>(name: K): Struct<
T & { [KK in K]: number },
T & { [KK in K]: number }
> {
const result = this.clone();
result.fields.push({ type: StructFieldType.Int32, name, signed: false });
result._size += 4;
return result;
}
public fixedLengthString<K extends PropertyKey, U = string>(
name: K,
length: number,
_type?: U
private number<
TName extends PropertyKey,
TTypeScriptType = StructField.Number.TypeScriptType
>(
name: TName,
type: StructField.Number.SubType,
options: StructField.BaseOptions = {},
_typescriptType?: () => TTypeScriptType,
): Struct<
T & { [KK in K]: U },
T & { [KK in K]: U }
> {
const result = this.clone();
result.fields.push({ type: StructFieldType.FixedLengthString, name, length });
result._size += 4;
return result;
}
public lengthPrefixedBuffer<K extends PropertyKey>(
name: K,
lengthType: 'int32',
): Struct<
T & { [KK in K]: ArrayBuffer },
T & { [KK in K]: ArrayBuffer }
TObject & Record<TName, TTypeScriptType>,
TAfterParsed,
TInit & Record<TName, TTypeScriptType>
> {
const result = this.clone();
result.fields.push({
type: StructFieldType.LengthPrefixedBuffer,
type: StructField.Type.Number,
name,
lengthType,
subType: 'buffer'
subType: type,
options,
});
result._size += 4;
result._size += StructField.Number.SizeMap[type];
return result;
}
public lengthPrefixedString<K extends PropertyKey, U = string>(
name: K,
lengthType: 'int32',
_type?: U
public int32<
TName extends PropertyKey,
TTypeScriptType = StructField.Number.TypeScriptType
>(
name: TName,
options?: StructField.BaseOptions,
_typescriptType?: () => TTypeScriptType,
): Struct<
T & { [KK in K]: U },
T & { [KK in K]: U }
TObject & Record<TName, TTypeScriptType>,
TAfterParsed,
TInit & Record<TName, TTypeScriptType>
> {
const result = this.clone();
result.fields.push({
type: StructFieldType.LengthPrefixedBuffer,
return this.number(
name,
lengthType,
subType: 'string',
});
result._size += 4;
return result;
StructField.Number.SubType.Int32,
options,
_typescriptType
);
}
public extra<U extends object>(value: U): Struct<T & U, T> {
public uint32<
TName extends PropertyKey,
TTypeScriptType = StructField.Number.TypeScriptType
>(
name: TName,
options?: StructField.BaseOptions,
_typescriptType?: () => TTypeScriptType,
): Struct<
TObject & Record<TName, TTypeScriptType>,
TAfterParsed,
TInit & Record<TName, TTypeScriptType>
> {
return this.number(
name,
StructField.Number.SubType.Uint32,
options,
_typescriptType
);
}
private array: ArrayInitializer<TObject, TAfterParsed, TInit> = (
name: PropertyKey,
type: StructField.Array.SubType,
options: StructField.FixedLengthArray.Options | StructField.VariableLengthArray.Options
): Struct<any, any, any> => {
const result = this.clone();
result._extra = { ...result._extra, value };
return result;
}
public create(value: TInit): T {
return { ...value, ...this._extra } as any;
}
public async parse(reader: StructReader): Promise<T> {
const result: any = {};
let buffer: ArrayBuffer;
let view: DataView;
let length: number;
for (const field of this.fields) {
switch (field.type) {
case StructFieldType.Int32:
buffer = await reader.read(4);
view = new DataView(buffer);
if (field.signed) {
result[field.name] = view.getInt32(0, this.littleEndian);
if ('length' in options) {
result.fields.push({
type: StructField.Type.FixedLengthArray,
name,
subType: type,
options: options,
});
result._size += options.length;
} else {
result[field.name] = view.getUint32(0, this.littleEndian);
result.fields.push({
type: StructField.Type.VariableLengthArray,
name,
subType: type,
options: options,
});
}
break;
case StructFieldType.FixedLengthString:
buffer = await reader.read(field.length);
result[field.name] = reader.decodeUtf8(buffer);
break;
case StructFieldType.LengthPrefixedBuffer:
switch (field.lengthType) {
case 'int32':
buffer = await reader.read(4);
view = new DataView(buffer);
length = view.getUint32(0, this.littleEndian);
break;
default:
throw new Error();
return result;
};
public arrayBuffer: ArrayTypeInitializer<
TObject,
TAfterParsed,
TInit,
StructField.Array.SubType.ArrayBuffer
> = (
name: PropertyKey,
options: any
) => {
return this.array(name, StructField.Array.SubType.ArrayBuffer, options);
};
public string: ArrayTypeInitializer<
TObject,
TAfterParsed,
TInit,
StructField.Array.SubType.String
> = (
name: PropertyKey,
options: any
) => {
return this.array(name, StructField.Array.SubType.String, options);
};
public extra<U extends object>(
value: U & ThisType<Omit<TObject, keyof U> & U>
): Struct<Omit<TObject, keyof U> & U, TAfterParsed, TInit> {
const result = this.clone();
result._extra = { ...result._extra, ...Object.getOwnPropertyDescriptors(value) };
return result;
}
buffer = await reader.read(length);
switch (field.subType) {
case 'buffer':
result[field.name] = buffer;
break;
case 'string':
result[field.name] = reader.decodeUtf8(buffer);
break;
public afterParsed(
callback: StructAfterParsed<TObject, never>
): Struct<TObject, never, TInit>;
public afterParsed(
callback?: StructAfterParsed<TObject, void>
): Struct<TObject, undefined, TInit>;
public afterParsed<TResult>(
callback?: StructAfterParsed<TObject, TResult>
): Struct<TObject, TResult, TInit>;
public afterParsed(
callback?: StructAfterParsed<TObject, any>
): Struct<any, any, any> {
const result = this.clone();
result._afterParsed = callback;
return result;
}
break;
public create(init: TInit, writer: StructWriter): TObject {
const object: any = {
[BackingField]: {},
};
for (const field of this.fields) {
const type = StructField.getType(field.type);
if (type.initialize) {
type.initialize({
field,
init,
object,
options: this.options,
writer,
});
} else {
object[field.name] = (init as any)[field.name];
}
}
return { ...result, ...this._extra };
Object.defineProperties(object, this._extra);
return object;
}
public async parse(
reader: StructReader
): Promise<TAfterParsed extends undefined ? TObject : TAfterParsed> {
const object: any = {
[BackingField]: {},
};
for (const field of this.fields) {
await StructField.getType(field.type).parse({
reader,
field,
object,
options: this.options,
});
}
Object.defineProperties(object, this._extra);
if (this._afterParsed) {
const result = this._afterParsed.call(object, object);
if (result) {
return result;
}
}
return object;
}
public toBuffer(init: TInit, writer: StructWriter): ArrayBuffer {
const value = this.create(init) as any;
const object = this.create(init, writer) as any;
let size = this._size;
for (const field of this.fields) {
switch (field.type) {
case StructFieldType.FixedLengthString:
value[field.name] = writer.encodeUtf8(value[field.name]).slice(0, field.length);
break;
case StructFieldType.LengthPrefixedBuffer:
switch (field.subType) {
case 'string':
const buffer = writer.encodeUtf8(value[field.name]);
value[field.name] = buffer;
break;
}
size += value[field.name].byteLength;
break;
}
}
const result = new Uint8Array(size);
const view = new DataView(result.buffer);
let offset = 0;
let buffer: ArrayBuffer;
let length: number;
for (const field of this.fields) {
switch (field.type) {
case StructFieldType.Int32:
if (field.signed) {
view.setInt32(offset, value[field.name], this.littleEndian);
let fieldSize: number[] = [];
for (let i = 0; i < this.fields.length; i++) {
const field = this.fields[i];
const type = StructField.getType(field.type);
if (type.getVariableLength) {
fieldSize[i] = type.getVariableLength({
writer,
field,
object,
options: this.options,
});
size += fieldSize[i];
} else {
view.setUint32(offset, value[field.name], this.littleEndian);
fieldSize[i] = type.getLength({ field, options: this.options });
}
}
offset += 4;
break;
case StructFieldType.LengthPrefixedBuffer:
buffer = value[field.name];
length = buffer.byteLength;
view.setUint32(offset, length, this.littleEndian);
offset += 4;
result.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
break;
case StructFieldType.FixedLengthString:
buffer = value[field.name];
result.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
break;
const buffer = new ArrayBuffer(size);
const dataView = new DataView(buffer);
let offset = 0;
for (let i = 0; i < this.fields.length; i++) {
const field = this.fields[i];
const type = StructField.getType(field.type);
type.write({
dataView,
field,
object,
offset,
options: this.options,
writer,
});
offset += fieldSize[i];
}
}
return result;
return buffer;
}
}

View file

@ -30,25 +30,6 @@
"tslib": "^1.10.0"
}
},
"@fluentui/dom-utilities": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-1.1.1.tgz",
"integrity": "sha512-w40gi8fzCpwa7U8cONiuu8rszPStkVOL/weDf5pCbYEb1gdaV7MDPSNkgM6IV0Kz+k017noDgK9Fv4ru1Dwz1g==",
"requires": {
"@uifabric/set-version": "^7.0.23",
"tslib": "^1.10.0"
},
"dependencies": {
"@uifabric/set-version": {
"version": "7.0.23",
"resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.23.tgz",
"integrity": "sha512-9E+YKtnH2kyMKnK9XZZsqyM8OCxEJIIfxtaThTlQpYOzrWAGJxQADFbZ7+Usi0U2xHnWNPFROjq+B9ocEzhqMA==",
"requires": {
"tslib": "^1.10.0"
}
}
}
},
"@fluentui/keyboard-key": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.2.12.tgz",
@ -112,9 +93,9 @@
}
},
"@microsoft/load-themed-styles": {
"version": "1.10.93",
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.93.tgz",
"integrity": "sha512-iziiQyDJmyP8QE33hYjuVsj18RvtzRMdON1QLDkJSrs9xisXWgEjK8U12UsEkBYpYXzxPxqq5+X+fK8Vs6g8vQ=="
"version": "1.10.97",
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.97.tgz",
"integrity": "sha512-FX8a2rXhYzXJWSoSjbxSyOvOo2SOHUjLG7JRWTf6rwiQDM/8fSTC/7TLkE2BAMg9n4vG+AxrgfN561VPnHQxrw=="
},
"@nodelib/fs.scandir": {
"version": "2.1.3",
@ -347,46 +328,6 @@
"@uifabric/set-version": "^7.0.23",
"@uifabric/utilities": "^7.32.2",
"tslib": "^1.10.0"
},
"dependencies": {
"@fluentui/react-window-provider": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-0.3.3.tgz",
"integrity": "sha512-MVPf2hqOQ17LAZsuvGcr3oOHksAskUm+fCYdXFhbVoAgsCDVTIuH6i8XgHFd6YjBtzjZmI4+k/3NTQfDqBX8EQ==",
"requires": {
"@uifabric/set-version": "^7.0.23",
"tslib": "^1.10.0"
}
},
"@uifabric/merge-styles": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.19.1.tgz",
"integrity": "sha512-yqUwmk62Kgu216QNPE9vOfS3h0kiSbTvoqM5QcZi+IzpqsBOlzZx3A9Er9UiDaqHRd5lsYF5pO/jeUULmBWF/A==",
"requires": {
"@uifabric/set-version": "^7.0.23",
"tslib": "^1.10.0"
}
},
"@uifabric/set-version": {
"version": "7.0.23",
"resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.23.tgz",
"integrity": "sha512-9E+YKtnH2kyMKnK9XZZsqyM8OCxEJIIfxtaThTlQpYOzrWAGJxQADFbZ7+Usi0U2xHnWNPFROjq+B9ocEzhqMA==",
"requires": {
"tslib": "^1.10.0"
}
},
"@uifabric/utilities": {
"version": "7.32.2",
"resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.32.2.tgz",
"integrity": "sha512-x47zJIjezkfed17EfNRTr9wP6hHR+i0pBbr3eQYetpcVsAXcbZsd+D6divwy+kQOsdLQ8TozWqoVk3ySe6RfSw==",
"requires": {
"@fluentui/dom-utilities": "^1.1.1",
"@uifabric/merge-styles": "^7.19.1",
"@uifabric/set-version": "^7.0.23",
"prop-types": "^15.7.2",
"tslib": "^1.10.0"
}
}
}
},
"@uifabric/set-version": {
@ -1645,15 +1586,6 @@
"merge2": "^1.3.0",
"slash": "^3.0.0"
}
},
"serialize-javascript": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
}
}
},
@ -4703,9 +4635,9 @@
"dev": true
},
"office-ui-fabric-react": {
"version": "7.138.0",
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.138.0.tgz",
"integrity": "sha512-HW4ugd+x7Jg96yBWxmUNMfkTS0U8RMwf5mGsHBAvW9s5l1ektjTjKnb5beHxNrddXKqcjz9ZThdTk/Gxds0jig==",
"version": "7.139.0",
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.139.0.tgz",
"integrity": "sha512-kqt3LUKUJfPie/32bmWxMcn3VdZoH5yiicdOj8B/huu1WPDDmhM+UlsUX2AmLeAEmqkH8XZxlgpmym96dhstaA==",
"requires": {
"@fluentui/date-time-utilities": "^7.8.1",
"@fluentui/react-focus": "^7.16.5",
@ -4721,19 +4653,6 @@
"@uifabric/utilities": "^7.32.2",
"prop-types": "^15.7.2",
"tslib": "^1.10.0"
},
"dependencies": {
"@uifabric/react-hooks": {
"version": "7.13.4",
"resolved": "https://registry.npmjs.org/@uifabric/react-hooks/-/react-hooks-7.13.4.tgz",
"integrity": "sha512-hyL3eQqbS7DrZCpkF1QDrC0TX+dV+yZZr5UgT3wAZMtzEMBFVgaiPHdv0zHgUbiQv5ktbQoY7yQp7clfVN65DA==",
"requires": {
"@fluentui/react-window-provider": "^0.3.3",
"@uifabric/set-version": "^7.0.23",
"@uifabric/utilities": "^7.32.2",
"tslib": "^1.10.0"
}
}
}
},
"on-finished": {
@ -5147,14 +5066,15 @@
}
},
"postcss-selector-parser": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
"integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.3.tgz",
"integrity": "sha512-0ClFaY4X1ra21LRqbW6y3rUbWcxnSVkDFG57R7Nxus9J9myPFlv+jYDMohzpkBx0RrjjiqjtycpchQ+PLGmZ9w==",
"dev": true,
"requires": {
"cssesc": "^3.0.0",
"indexes-of": "^1.0.1",
"uniq": "^1.0.1"
"uniq": "^1.0.1",
"util-deprecate": "^1.0.2"
}
},
"postcss-value-parser": {
@ -5717,9 +5637,9 @@
}
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
@ -6098,12 +6018,12 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -6129,12 +6049,12 @@
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"ms": {
@ -6590,6 +6510,15 @@
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
@ -7812,12 +7741,12 @@
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
},
"fill-range": {

View file

@ -1,7 +1,8 @@
import { Breadcrumb, concatStyleSets, ContextualMenu, DetailsListLayoutMode, DirectionalHint, IBreadcrumbItem, IColumn, Icon, IContextualMenuItem, IDetailsHeaderProps, IDetailsList, IRenderFunction, Layer, MarqueeSelection, mergeStyleSets, Overlay, Selection, ShimmeredDetailsList, StackItem } from '@fluentui/react';
import { FileIconType, getFileTypeIconProps, initializeFileTypeIcons } from '@uifabric/file-type-icons';
import { useConst, useConstCallback } from '@uifabric/react-hooks';
import { useConst } from '@uifabric/react-hooks';
import { AdbSyncEntryResponse, LinuxFileType } from '@yume-chan/adb';
import { encodeUtf8 } from '@yume-chan/adb-backend-web';
import path from 'path';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import StreamSaver from 'streamsaver';
@ -17,7 +18,7 @@ interface ListItem extends AdbSyncEntryResponse {
}
function toListItem(item: AdbSyncEntryResponse): ListItem {
return { ...item, key: item.name };
return { ...item, key: item.name! };
}
const classNames = mergeStyleSets({
@ -171,19 +172,19 @@ export default withDisplayName('FileManager', ({
for (const entry of linkItems) {
try {
const followLinkPath = path.resolve(currentPath, entry.name) + '/';
const followLinkPath = path.resolve(currentPath, entry.name!) + '/';
console.log(followLinkPath);
await sync.lstat(followLinkPath);
items.push(toListItem(entry));
console.log(entry);
} catch (e) {
console.log(e);
items.push(toListItem(new AdbSyncEntryResponse(
(LinuxFileType.File << 12) | entry.mode,
0,
entry.lastModifiedTime,
entry.name
)));
items.push(toListItem(AdbSyncEntryResponse.create({
mode: (LinuxFileType.File << 12) | entry.mode,
size: 0,
lastModifiedTime: entry.lastModifiedTime,
name: entry.name,
}, { encodeUtf8 })));
}
}
@ -214,11 +215,11 @@ export default withDisplayName('FileManager', ({
if (aIsFile !== bIsFile) {
result = aIsFile - bIsFile;
} else {
const aSortKey = a[sortKey];
const bSortKey = b[sortKey];
const aSortKey = a[sortKey]!;
const bSortKey = b[sortKey]!;
if (aSortKey === bSortKey) {
result = a.name < b.name ? -1 : 1;
result = a.name! < b.name! ? -1 : 1;
} else {
result = aSortKey < bSortKey ? -1 : 1;
}
@ -249,7 +250,7 @@ export default withDisplayName('FileManager', ({
case LinuxFileType.Directory:
return <Icon {...getFileTypeIconProps({ size: 20, type: FileIconType.folder })} />;
case LinuxFileType.File:
return <Icon {...getFileTypeIconProps({ size: 20, extension: extensionName(item.name) })} />;
return <Icon {...getFileTypeIconProps({ size: 20, extension: extensionName(item.name!) })} />;
default:
return <Icon {...getFileTypeIconProps({ size: 20, extension: 'txt' })} />;
}
@ -337,15 +338,15 @@ export default withDisplayName('FileManager', ({
switch (item.type) {
case LinuxFileType.Link:
case LinuxFileType.Directory:
setCurrentPath(path.resolve(currentPath, item.name));
setCurrentPath(path.resolve(currentPath, item.name!));
break;
case LinuxFileType.File:
switch (extensionName(item.name)) {
switch (extensionName(item.name!)) {
case '.jpg':
case '.png':
case '.svg':
case '.gif':
previewImage(path.resolve(currentPath, item.name));
previewImage(path.resolve(currentPath, item.name!));
break;
}
break;
@ -378,10 +379,10 @@ export default withDisplayName('FileManager', ({
(async () => {
const sync = await device!.sync();
try {
const itemPath = path.resolve(currentPath, selectedItems[0].name);
const itemPath = path.resolve(currentPath, selectedItems[0].name!);
const readableStream = createReadableStreamFromBufferIterator(sync.receive(itemPath));
const writeableStream = StreamSaver.createWriteStream(selectedItems[0].name, {
const writeableStream = StreamSaver.createWriteStream(selectedItems[0].name!, {
size: selectedItems[0].size,
});
await readableStream.pipeTo(writeableStream);
@ -403,7 +404,7 @@ export default withDisplayName('FileManager', ({
(async () => {
try {
for (const item of selectedItems) {
const output = await device!.shell('rm', '-rf', `"${path.resolve(currentPath, item.name)}"`);
const output = await device!.shell('rm', '-rf', `"${path.resolve(currentPath, item.name!)}"`);
if (output) {
showErrorDialog(output);
return;
@ -427,9 +428,9 @@ export default withDisplayName('FileManager', ({
setContextMenuTarget(e as MouseEvent);
return false;
}, [currentPath, device]);
const hideContextMenu = useConstCallback(() => {
const hideContextMenu = React.useCallback(() => {
setContextMenuTarget(undefined);
});
}, []);
return (
<MarqueeSelection selection={selection}>

View file

@ -1,4 +1,5 @@
import { IconButton, SearchBox, Stack, StackItem } from '@fluentui/react';
import { encodeUtf8 } from '@yume-chan/adb-backend-web';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
@ -65,9 +66,8 @@ export default withDisplayName('Shell', ({
(async () => {
const shell = await device.shell();
const textEncoder = new TextEncoder();
terminal.onData(data => {
const { buffer } = textEncoder.encode(data);
const buffer = encodeUtf8(data);
shell.write(buffer);
});
shell.onData(data => {