mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 19:42:15 +02:00
feat(sync): rewrite struct parsing
This commit is contained in:
parent
4da02d5cdc
commit
5006771c01
6 changed files with 832 additions and 401 deletions
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
141
packages/demo/package-lock.json
generated
141
packages/demo/package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue