mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 10:49:24 +02:00
refactor(adb): further split sync module
This commit is contained in:
parent
65e0ba042e
commit
d887639efd
18 changed files with 482 additions and 419 deletions
|
@ -2,12 +2,10 @@ import { PromiseResolver } from '@yume-chan/async-operation-manager';
|
|||
import { DisposableList } from '@yume-chan/event';
|
||||
import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth';
|
||||
import { AdbBackend } from './backend';
|
||||
import { AdbReverseCommand, AdbTcpIpCommand } from './commands';
|
||||
import { AdbFrameBuffer, AdbReverseCommand, AdbSync, AdbTcpIpCommand } from './commands';
|
||||
import { AdbFeatures } from './features';
|
||||
import { FrameBuffer } from './framebuffer';
|
||||
import { AdbCommand } from './packet';
|
||||
import { AdbBufferedStream, AdbPacketDispatcher, AdbReadableStream, AdbStream } from './stream';
|
||||
import { AdbSync } from './sync';
|
||||
|
||||
export enum AdbPropKey {
|
||||
Product = 'ro.product.name',
|
||||
|
@ -175,15 +173,20 @@ export class Adb {
|
|||
}
|
||||
}
|
||||
|
||||
public async sync(): Promise<AdbSync> {
|
||||
const stream = await this.createStream('sync:');
|
||||
return new AdbSync(stream, this);
|
||||
public async getProp(key: string): Promise<string> {
|
||||
const output = await this.shell('getprop', key);
|
||||
return output.trim();
|
||||
}
|
||||
|
||||
public async framebuffer(): Promise<FrameBuffer> {
|
||||
public async sync(): Promise<AdbSync> {
|
||||
const stream = await this.createStream('sync:');
|
||||
return new AdbSync(this, stream);
|
||||
}
|
||||
|
||||
public async framebuffer(): Promise<AdbFrameBuffer> {
|
||||
const stream = await this.createStream('framebuffer:');
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
return FrameBuffer.deserialize(buffered);
|
||||
return AdbFrameBuffer.deserialize(buffered);
|
||||
}
|
||||
|
||||
public async createStream(service: string): Promise<AdbStream> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Struct, StructValueType } from "@yume-chan/struct";
|
||||
|
||||
export const FrameBuffer =
|
||||
export const AdbFrameBuffer =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('version', undefined, 2 as const)
|
||||
.uint32('bpp')
|
||||
|
@ -18,4 +18,4 @@ export const FrameBuffer =
|
|||
.uint32('alpha_length')
|
||||
.arrayBuffer('data', { lengthField: 'size' });
|
||||
|
||||
export type FrameBuffer = StructValueType<typeof FrameBuffer>;
|
||||
export type AdbFrameBuffer = StructValueType<typeof AdbFrameBuffer>;
|
|
@ -1,3 +1,5 @@
|
|||
export * from './base';
|
||||
export * from './tcpip';
|
||||
export * from './framebuffer';
|
||||
export * from './reverse';
|
||||
export * from './sync';
|
||||
export * from './tcpip';
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { AutoDisposable } from '@yume-chan/event';
|
||||
import { Struct } from '@yume-chan/struct';
|
||||
import { Adb } from '../adb';
|
||||
import { AdbPacket } from '../packet';
|
||||
import { AdbBufferedStream, AdbPacketDispatcher, AdbStream } from '../stream';
|
||||
import { AdbCommandBase } from './base';
|
||||
import { AdbBufferedStream, AdbIncomingStreamEventArgs, AdbPacketDispatcher, AdbStream } from '../stream';
|
||||
|
||||
export interface AdbReverseHandler {
|
||||
onStream(packet: AdbPacket, stream: AdbStream): void;
|
||||
|
@ -29,29 +27,32 @@ const AdbReverseErrorResponse =
|
|||
});
|
||||
|
||||
export class AdbReverseCommand extends AutoDisposable {
|
||||
private portToHandlerMap = new Map<number, AdbReverseHandler>();
|
||||
protected portToHandlerMap = new Map<number, AdbReverseHandler>();
|
||||
|
||||
private devicePortToPortMap = new Map<number, number>();
|
||||
protected devicePortToPortMap = new Map<number, number>();
|
||||
|
||||
private dispatcher: AdbPacketDispatcher;
|
||||
protected dispatcher: AdbPacketDispatcher;
|
||||
|
||||
private listening = false;
|
||||
protected listening = false;
|
||||
|
||||
public constructor(dispatcher: AdbPacketDispatcher) {
|
||||
super();
|
||||
|
||||
this.dispatcher = dispatcher;
|
||||
this.addDisposable(this.dispatcher.onStream(this.handleStream, this));
|
||||
}
|
||||
|
||||
public async list(): Promise<AdbForwardListener[]> {
|
||||
const stream = await this.dispatcher.createStream('reverse:list-forward');
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
protected handleStream(e: AdbIncomingStreamEventArgs): void {
|
||||
if (e.handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await AdbReverseStringResponse.deserialize(buffered);
|
||||
|
||||
return response.content!.split('\n').map(line => {
|
||||
const [deviceSerial, localName, remoteName] = line.split(' ');
|
||||
return { deviceSerial, localName, remoteName };
|
||||
});
|
||||
const address = this.dispatcher.backend.decodeUtf8(e.packet.payload!);
|
||||
const port = Number.parseInt(address.substring(4));
|
||||
if (this.portToHandlerMap.has(port)) {
|
||||
this.portToHandlerMap.get(port)!.onStream(e.packet, e.stream);
|
||||
e.handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async add(
|
||||
|
@ -59,21 +60,6 @@ export class AdbReverseCommand extends AutoDisposable {
|
|||
handler: AdbReverseHandler,
|
||||
devicePort: number = 0,
|
||||
): Promise<number> {
|
||||
if (!this.listening) {
|
||||
this.addDisposable(this.dispatcher.onStream(e => {
|
||||
if (e.handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const address = this.dispatcher.backend.decodeUtf8(e.packet.payload!);
|
||||
const port = Number.parseInt(address.substring(4));
|
||||
if (this.portToHandlerMap.has(port)) {
|
||||
this.portToHandlerMap.get(port)!.onStream(e.packet, e.stream);
|
||||
e.handled = true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const stream = await this.dispatcher.createStream(`reverse:forward:tcp:${devicePort};tcp:${port}`);
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
|
||||
|
@ -92,6 +78,18 @@ export class AdbReverseCommand extends AutoDisposable {
|
|||
}
|
||||
}
|
||||
|
||||
public async list(): Promise<AdbForwardListener[]> {
|
||||
const stream = await this.dispatcher.createStream('reverse:list-forward');
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
|
||||
const response = await AdbReverseStringResponse.deserialize(buffered);
|
||||
|
||||
return response.content!.split('\n').map(line => {
|
||||
const [deviceSerial, localName, remoteName] = line.split(' ');
|
||||
return { deviceSerial, localName, remoteName };
|
||||
});
|
||||
}
|
||||
|
||||
public async remove(devicePort: number): Promise<void> {
|
||||
const stream = await this.dispatcher.createStream(`reverse:killforward:tcp:${devicePort}`);
|
||||
const buffered = new AdbBufferedStream(stream);
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
export * from './sync';
|
||||
export * from './list';
|
||||
export * from './receive';
|
||||
export * from './request';
|
||||
export * from './response';
|
||||
export * from './send';
|
||||
export * from './stat';
|
||||
export * from './sync';
|
||||
|
|
39
packages/adb/src/commands/sync/list.ts
Normal file
39
packages/adb/src/commands/sync/list.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { StructValueType } from '@yume-chan/struct';
|
||||
import { AdbBufferedStream } from '../../stream';
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
|
||||
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response';
|
||||
import { AdbSyncLstatResponse } from './stat';
|
||||
|
||||
export const AdbSyncEntryResponse =
|
||||
AdbSyncLstatResponse
|
||||
.afterParsed()
|
||||
.uint32('nameLength')
|
||||
.string('name', { lengthField: 'nameLength' })
|
||||
.extra({ id: AdbSyncResponseId.Entry as const });
|
||||
|
||||
export type AdbSyncEntryResponse = StructValueType<typeof AdbSyncEntryResponse>;
|
||||
|
||||
const ResponseTypes = {
|
||||
[AdbSyncResponseId.Entry]: AdbSyncEntryResponse,
|
||||
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncEntryResponse.size),
|
||||
};
|
||||
|
||||
export async function* adbSyncOpenDir(
|
||||
stream: AdbBufferedStream,
|
||||
path: string
|
||||
): AsyncGenerator<AdbSyncEntryResponse, void, void> {
|
||||
await adbSyncWriteRequest(stream, AdbSyncRequestId.List, path);
|
||||
|
||||
while (true) {
|
||||
const response = await adbSyncReadResponse(stream, ResponseTypes);
|
||||
switch (response.id) {
|
||||
case AdbSyncResponseId.Entry:
|
||||
yield response;
|
||||
break;
|
||||
case AdbSyncResponseId.Done:
|
||||
return;
|
||||
default:
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
||||
}
|
||||
}
|
34
packages/adb/src/commands/sync/receive.ts
Normal file
34
packages/adb/src/commands/sync/receive.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Struct } from '@yume-chan/struct';
|
||||
import { AdbBufferedStream } from '../../stream';
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
|
||||
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response';
|
||||
|
||||
export const AdbSyncDataResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('dataLength')
|
||||
.arrayBuffer('data', { lengthField: 'dataLength' })
|
||||
.extra({ id: AdbSyncResponseId.Data as const });
|
||||
|
||||
const ResponseTypes = {
|
||||
[AdbSyncResponseId.Data]: AdbSyncDataResponse,
|
||||
[AdbSyncResponseId.Done]: new AdbSyncDoneResponse(AdbSyncDataResponse.size),
|
||||
};
|
||||
|
||||
export async function* adbSyncPull(
|
||||
stream: AdbBufferedStream,
|
||||
path: string,
|
||||
): AsyncGenerator<ArrayBuffer, void, void> {
|
||||
await adbSyncWriteRequest(stream, AdbSyncRequestId.Receive, path);
|
||||
while (true) {
|
||||
const response = await adbSyncReadResponse(stream, ResponseTypes);
|
||||
switch (response.id) {
|
||||
case AdbSyncResponseId.Data:
|
||||
yield response.data!;
|
||||
break;
|
||||
case AdbSyncResponseId.Done:
|
||||
return;
|
||||
default:
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
||||
}
|
||||
}
|
47
packages/adb/src/commands/sync/request.ts
Normal file
47
packages/adb/src/commands/sync/request.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { Struct } from '@yume-chan/struct';
|
||||
import { AdbBufferedStream } from '../../stream';
|
||||
|
||||
export enum AdbSyncRequestId {
|
||||
List = 'LIST',
|
||||
Send = 'SEND',
|
||||
Lstat = 'STAT',
|
||||
Stat = 'STA2',
|
||||
Lstat2 = 'LST2',
|
||||
Data = 'DATA',
|
||||
Done = 'DONE',
|
||||
Receive = 'RECV',
|
||||
}
|
||||
|
||||
export const AdbSyncNumberRequest =
|
||||
new Struct({ littleEndian: true })
|
||||
.string('id', { length: 4 })
|
||||
.uint32('arg');
|
||||
|
||||
export const AdbSyncDataRequest =
|
||||
AdbSyncNumberRequest
|
||||
.arrayBuffer('data', { lengthField: 'arg' });
|
||||
|
||||
export async function adbSyncWriteRequest(
|
||||
stream: AdbBufferedStream,
|
||||
id: AdbSyncRequestId | string,
|
||||
value: number | string | ArrayBuffer
|
||||
): Promise<void> {
|
||||
let buffer: ArrayBuffer;
|
||||
if (typeof value === 'number') {
|
||||
buffer = AdbSyncNumberRequest.serialize({
|
||||
id,
|
||||
arg: value,
|
||||
}, stream);
|
||||
} else if (typeof value === 'string') {
|
||||
buffer = AdbSyncDataRequest.serialize({
|
||||
id,
|
||||
data: stream.encodeUtf8(value),
|
||||
}, stream);
|
||||
} else {
|
||||
buffer = AdbSyncDataRequest.serialize({
|
||||
id,
|
||||
data: value,
|
||||
}, stream);
|
||||
}
|
||||
await stream.write(buffer);
|
||||
}
|
57
packages/adb/src/commands/sync/response.ts
Normal file
57
packages/adb/src/commands/sync/response.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Struct, StructDeserializationContext, StructValueType } from '@yume-chan/struct';
|
||||
import { AdbBufferedStream } from '../../stream';
|
||||
|
||||
export enum AdbSyncResponseId {
|
||||
Entry = 'DENT',
|
||||
Lstat = 'STAT',
|
||||
Stat = 'STA2',
|
||||
Lstat2 = 'LST2',
|
||||
Done = 'DONE',
|
||||
Data = 'DATA',
|
||||
Ok = 'OKAY',
|
||||
Fail = 'FAIL',
|
||||
}
|
||||
|
||||
// DONE responses' size are always same as the request's normal response.
|
||||
// For example DONE responses for LIST requests are 16 bytes (same as DENT responses),
|
||||
// but DONE responses for STAT requests are 12 bytes (same as STAT responses)
|
||||
// So we need to know responses' size in advance.
|
||||
export class AdbSyncDoneResponse {
|
||||
private length: number;
|
||||
|
||||
public readonly id = AdbSyncResponseId.Done;
|
||||
|
||||
public constructor(length: number) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public async deserialize(context: StructDeserializationContext): Promise<this> {
|
||||
await context.read(this.length);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const AdbSyncFailResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('messageLength')
|
||||
.string('message', { lengthField: 'messageLength' })
|
||||
.afterParsed(object => {
|
||||
throw new Error(object.message);
|
||||
});
|
||||
|
||||
export async function adbSyncReadResponse<T extends Record<string, { deserialize(context: StructDeserializationContext): Promise<any>; }>>(
|
||||
stream: AdbBufferedStream,
|
||||
types: T,
|
||||
): Promise<StructValueType<T[keyof T]>> {
|
||||
const id = stream.backend.decodeUtf8(await stream.read(4));
|
||||
|
||||
if (id === AdbSyncResponseId.Fail) {
|
||||
await AdbSyncFailResponse.deserialize(stream);
|
||||
}
|
||||
|
||||
if (types[id]) {
|
||||
return types[id].deserialize(stream);
|
||||
}
|
||||
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
107
packages/adb/src/commands/sync/send.ts
Normal file
107
packages/adb/src/commands/sync/send.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { Struct } from '@yume-chan/struct';
|
||||
import { AdbBufferedStream } from '../../stream';
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
|
||||
import { adbSyncReadResponse, AdbSyncResponseId } from './response';
|
||||
import { LinuxFileType } from './stat';
|
||||
|
||||
export const AdbSyncOkResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('unused');
|
||||
|
||||
const ResponseTypes = {
|
||||
[AdbSyncResponseId.Ok]: AdbSyncOkResponse,
|
||||
};
|
||||
|
||||
export function* chunkArrayLike(
|
||||
value: ArrayLike<number> | ArrayBufferLike,
|
||||
size: number
|
||||
): Generator<ArrayBuffer, void, void> {
|
||||
if ('length' in value) {
|
||||
value = new Uint8Array(value).buffer;
|
||||
}
|
||||
|
||||
if (value.byteLength <= size) {
|
||||
return yield value;
|
||||
}
|
||||
|
||||
for (let i = 0; i < value.byteLength; i += size) {
|
||||
yield value.slice(i, i + size);
|
||||
}
|
||||
}
|
||||
|
||||
export async function* chunkAsyncIterable(
|
||||
value: AsyncIterable<ArrayBuffer>,
|
||||
size: number
|
||||
): AsyncGenerator<ArrayBuffer, void, void> {
|
||||
let result = new Uint8Array(size);
|
||||
let index = 0;
|
||||
for await (let buffer of value) {
|
||||
// `result` has some data, `result + buffer` is enough
|
||||
if (index !== 0 && index + buffer.byteLength >= size) {
|
||||
const remainder = size - index;
|
||||
result.set(new Uint8Array(buffer, 0, remainder), index);
|
||||
yield result.buffer;
|
||||
|
||||
result = new Uint8Array(size);
|
||||
index = 0;
|
||||
|
||||
if (buffer.byteLength > remainder) {
|
||||
// `buffer` still has some data
|
||||
buffer = buffer.slice(remainder);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// `result` is empty, `buffer` alone is enough
|
||||
if (buffer.byteLength >= size) {
|
||||
let remainder = false;
|
||||
for (const chunk of chunkArrayLike(buffer, size)) {
|
||||
if (chunk.byteLength === size) {
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
// `buffer` still has some data
|
||||
remainder = true;
|
||||
buffer = chunk;
|
||||
}
|
||||
|
||||
if (!remainder) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// `result` has some data but `result + buffer` is still not enough
|
||||
// or after previous steps `buffer` still has some data
|
||||
result.set(new Uint8Array(buffer), index);
|
||||
index += buffer.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
export const AdbSyncSendPacketSize = 64 * 1024;
|
||||
|
||||
export async function adbSyncPush(
|
||||
stream: AdbBufferedStream,
|
||||
path: string,
|
||||
file: ArrayLike<number> | ArrayBufferLike | AsyncIterable<ArrayBuffer>,
|
||||
mode: number = LinuxFileType.File | 0o777,
|
||||
mtime: number = (Date.now() / 1000) | 0,
|
||||
packetSize: number = AdbSyncSendPacketSize,
|
||||
): Promise<void> {
|
||||
const pathAndMode = `${path},${mode.toString(8)}`;
|
||||
await adbSyncWriteRequest(stream, AdbSyncRequestId.Send, pathAndMode);
|
||||
|
||||
let chunkReader: Iterable<ArrayBuffer> | AsyncIterable<ArrayBuffer>;
|
||||
if ('length' in file || 'byteLength' in file) {
|
||||
chunkReader = chunkArrayLike(file, packetSize);
|
||||
} else {
|
||||
chunkReader = chunkAsyncIterable(file, packetSize);
|
||||
}
|
||||
|
||||
for await (const buffer of chunkReader) {
|
||||
await adbSyncWriteRequest(stream, AdbSyncRequestId.Data, buffer);
|
||||
}
|
||||
|
||||
await adbSyncWriteRequest(stream, AdbSyncRequestId.Send, mtime);
|
||||
await adbSyncReadResponse(stream, ResponseTypes);
|
||||
}
|
121
packages/adb/src/commands/sync/stat.ts
Normal file
121
packages/adb/src/commands/sync/stat.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { placeholder, Struct, StructValueType } from '@yume-chan/struct';
|
||||
import { AdbBufferedStream } from '../../stream';
|
||||
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
|
||||
import { adbSyncReadResponse, AdbSyncResponseId } from './response';
|
||||
|
||||
// https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36
|
||||
export enum LinuxFileType {
|
||||
Directory = 0o04,
|
||||
File = 0o10,
|
||||
Link = 0o12,
|
||||
}
|
||||
|
||||
export const AdbSyncLstatResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.int32('mode')
|
||||
.int32('size')
|
||||
.int32('mtime')
|
||||
.extra({
|
||||
id: AdbSyncResponseId.Lstat 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.mtime === 0
|
||||
) {
|
||||
throw new Error('lstat failed');
|
||||
}
|
||||
});
|
||||
|
||||
export type AdbSyncLstatResponse = StructValueType<typeof AdbSyncLstatResponse>;
|
||||
|
||||
export enum AdbSyncStatErrorCode {
|
||||
EACCES = 13,
|
||||
EEXIST = 17,
|
||||
EFAULT = 14,
|
||||
EFBIG = 27,
|
||||
EINTR = 4,
|
||||
EINVAL = 22,
|
||||
EIO = 5,
|
||||
EISDIR = 21,
|
||||
ELOOP = 40,
|
||||
EMFILE = 24,
|
||||
ENAMETOOLONG = 36,
|
||||
ENFILE = 23,
|
||||
ENOENT = 2,
|
||||
ENOMEM = 12,
|
||||
ENOSPC = 28,
|
||||
ENOTDIR = 20,
|
||||
EOVERFLOW = 75,
|
||||
EPERM = 1,
|
||||
EROFS = 30,
|
||||
ETXTBSY = 26,
|
||||
}
|
||||
|
||||
export const AdbSyncStatResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('error', undefined, placeholder<AdbSyncStatErrorCode>())
|
||||
.uint64('dev')
|
||||
.uint64('ino')
|
||||
.uint32('mode')
|
||||
.uint32('nlink')
|
||||
.uint32('uid')
|
||||
.uint32('gid')
|
||||
.uint64('size')
|
||||
.uint64('atime')
|
||||
.uint64('mtime')
|
||||
.uint64('ctime')
|
||||
.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.error) {
|
||||
throw new Error(AdbSyncStatErrorCode[object.error]);
|
||||
}
|
||||
});
|
||||
|
||||
export type AdbSyncStatResponse = StructValueType<typeof AdbSyncStatResponse>;
|
||||
|
||||
const StatResponseType = {
|
||||
[AdbSyncResponseId.Stat]: AdbSyncStatResponse,
|
||||
};
|
||||
|
||||
const LstatResponseType = {
|
||||
[AdbSyncResponseId.Lstat]: AdbSyncLstatResponse,
|
||||
};
|
||||
|
||||
const Lstat2ResponseType = {
|
||||
[AdbSyncResponseId.Lstat2]: AdbSyncStatResponse,
|
||||
};
|
||||
|
||||
export async function adbSyncLstat(
|
||||
stream: AdbBufferedStream,
|
||||
path: string,
|
||||
v2: boolean,
|
||||
): Promise<AdbSyncLstatResponse | AdbSyncStatResponse> {
|
||||
let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
|
||||
let responseType: typeof LstatResponseType | typeof Lstat2ResponseType;
|
||||
|
||||
if (v2) {
|
||||
requestId = AdbSyncRequestId.Lstat2;
|
||||
responseType = Lstat2ResponseType;
|
||||
} else {
|
||||
requestId = AdbSyncRequestId.Lstat;
|
||||
responseType = LstatResponseType;
|
||||
}
|
||||
|
||||
await adbSyncWriteRequest(stream, requestId, path);
|
||||
return adbSyncReadResponse(stream, responseType);
|
||||
}
|
||||
|
||||
export async function adbSyncStat(
|
||||
stream: AdbBufferedStream,
|
||||
path: string,
|
||||
): Promise<AdbSyncStatResponse> {
|
||||
await adbSyncWriteRequest(stream, AdbSyncRequestId.Stat, path);
|
||||
return adbSyncReadResponse(stream, StatResponseType);
|
||||
}
|
|
@ -1,260 +1,12 @@
|
|||
import { AutoDisposable } from '@yume-chan/event';
|
||||
import { placeholder, Struct, StructDeserializationContext, StructInitType, StructValueType } from '@yume-chan/struct';
|
||||
import { Adb } from '../../adb';
|
||||
import { AdbFeatures } from '../../features';
|
||||
import { AdbBufferedStream, AdbStream } from '../../stream';
|
||||
import { AutoResetEvent } from '../../utils';
|
||||
|
||||
export enum AdbSyncRequestId {
|
||||
List = 'LIST',
|
||||
Send = 'SEND',
|
||||
Lstat = 'STAT',
|
||||
Stat = 'STA2',
|
||||
Lstat2 = 'LST2',
|
||||
Data = 'DATA',
|
||||
Done = 'DONE',
|
||||
Receive = 'RECV',
|
||||
}
|
||||
|
||||
export enum AdbSyncResponseId {
|
||||
Entry = 'DENT',
|
||||
Lstat = 'STAT',
|
||||
Stat = 'STA2',
|
||||
Lstat2 = 'LST2',
|
||||
Done = 'DONE',
|
||||
Data = 'DATA',
|
||||
Ok = 'OKAY',
|
||||
Fail = 'FAIL',
|
||||
}
|
||||
|
||||
const AdbSyncNumberRequest =
|
||||
new Struct({ littleEndian: true })
|
||||
.string('id', { length: 4 })
|
||||
.uint32('arg');
|
||||
|
||||
const AdbSyncStringRequest =
|
||||
AdbSyncNumberRequest
|
||||
.string('data', { lengthField: 'arg' });
|
||||
|
||||
const AdbSyncBufferRequest =
|
||||
AdbSyncNumberRequest
|
||||
.arrayBuffer('data', { lengthField: 'arg' });
|
||||
|
||||
// https://github.com/python/cpython/blob/4e581d64b8aff3e2eda99b12f080c877bb78dfca/Lib/stat.py#L36
|
||||
export enum LinuxFileType {
|
||||
Directory = 0o04,
|
||||
File = 0o10,
|
||||
Link = 0o12,
|
||||
}
|
||||
|
||||
export const AdbSyncLstatResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.int32('mode')
|
||||
.int32('size')
|
||||
.int32('mtime')
|
||||
.extra({
|
||||
id: AdbSyncResponseId.Lstat 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.mtime === 0
|
||||
) {
|
||||
throw new Error('lstat failed');
|
||||
}
|
||||
});
|
||||
|
||||
export type AdbSyncLstatResponse = StructValueType<typeof AdbSyncLstatResponse>;
|
||||
|
||||
export enum ErrorCode {
|
||||
EACCES = 13,
|
||||
EEXIST = 17,
|
||||
EFAULT = 14,
|
||||
EFBIG = 27,
|
||||
EINTR = 4,
|
||||
EINVAL = 22,
|
||||
EIO = 5,
|
||||
EISDIR = 21,
|
||||
ELOOP = 40,
|
||||
EMFILE = 24,
|
||||
ENAMETOOLONG = 36,
|
||||
ENFILE = 23,
|
||||
ENOENT = 2,
|
||||
ENOMEM = 12,
|
||||
ENOSPC = 28,
|
||||
ENOTDIR = 20,
|
||||
EOVERFLOW = 75,
|
||||
EPERM = 1,
|
||||
EROFS = 30,
|
||||
ETXTBSY = 26,
|
||||
}
|
||||
|
||||
export const AdbSyncStatResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('error', undefined, placeholder<ErrorCode>())
|
||||
.uint64('dev')
|
||||
.uint64('ino')
|
||||
.uint32('mode')
|
||||
.uint32('nlink')
|
||||
.uint32('uid')
|
||||
.uint32('gid')
|
||||
.uint64('size')
|
||||
.uint64('atime')
|
||||
.uint64('mtime')
|
||||
.uint64('ctime')
|
||||
.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.error) {
|
||||
throw new Error(ErrorCode[object.error]);
|
||||
}
|
||||
});
|
||||
|
||||
export type AdbSyncStatResponse = StructValueType<typeof AdbSyncStatResponse>;
|
||||
|
||||
export const AdbSyncEntryResponse =
|
||||
AdbSyncLstatResponse
|
||||
.afterParsed()
|
||||
.uint32('nameLength')
|
||||
.string('name', { lengthField: 'nameLength' })
|
||||
.extra({ id: AdbSyncResponseId.Entry as const });
|
||||
|
||||
export type AdbSyncEntryResponse = StructValueType<typeof AdbSyncEntryResponse>;
|
||||
|
||||
export const AdbSyncDataResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('dataLength')
|
||||
.arrayBuffer('data', { lengthField: 'dataLength' })
|
||||
.extra({ id: AdbSyncResponseId.Data as const });
|
||||
|
||||
export interface AdbSyncDoneResponseDeserializeContext extends StructDeserializationContext {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export class AdbSyncDoneResponse {
|
||||
public static readonly instance = new AdbSyncDoneResponse();
|
||||
|
||||
public static async deserialize(
|
||||
context: AdbSyncDoneResponseDeserializeContext
|
||||
): Promise<AdbSyncDoneResponse> {
|
||||
await context.read(context.size);
|
||||
return AdbSyncDoneResponse.instance;
|
||||
}
|
||||
|
||||
public readonly id = AdbSyncResponseId.Done;
|
||||
}
|
||||
|
||||
export const AdbSyncFailResponse =
|
||||
new Struct({ littleEndian: true })
|
||||
.uint32('messageLength')
|
||||
.string('message', { lengthField: 'messageLength' })
|
||||
.afterParsed(object => {
|
||||
throw new Error(object.message);
|
||||
});
|
||||
|
||||
const ResponseTypeMap = {
|
||||
[AdbSyncResponseId.Entry]: AdbSyncEntryResponse,
|
||||
[AdbSyncResponseId.Lstat]: AdbSyncLstatResponse,
|
||||
[AdbSyncResponseId.Stat]: AdbSyncStatResponse,
|
||||
[AdbSyncResponseId.Lstat2]: AdbSyncStatResponse,
|
||||
[AdbSyncResponseId.Data]: AdbSyncDataResponse,
|
||||
[AdbSyncResponseId.Fail]: AdbSyncFailResponse,
|
||||
[AdbSyncResponseId.Done]: AdbSyncDoneResponse,
|
||||
} as const;
|
||||
|
||||
async function readResponse(stream: AdbBufferedStream, size: number) {
|
||||
// DONE responses' size are always same as the request's normal response.
|
||||
// For example DONE responses for LIST requests are 16 bytes (same as DENT responses),
|
||||
// but DONE responses for STAT requests are 12 bytes (same as STAT responses)
|
||||
// So we need to know responses' size in advance.
|
||||
const id = stream.backend.decodeUtf8(await stream.read(4)) as keyof typeof ResponseTypeMap;
|
||||
|
||||
if (ResponseTypeMap[id]) {
|
||||
return ResponseTypeMap[id].deserialize({
|
||||
size,
|
||||
read: stream.read.bind(stream),
|
||||
decodeUtf8: stream.backend.decodeUtf8.bind(stream.backend),
|
||||
encodeUtf8: stream.backend.encodeUtf8.bind(stream.backend),
|
||||
});
|
||||
}
|
||||
|
||||
await stream.read(size);
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
||||
|
||||
export function chunkArray(
|
||||
value: ArrayLike<number>,
|
||||
size: number
|
||||
): Generator<ArrayBuffer, void, void> {
|
||||
return chunkArrayBuffer(new Uint8Array(value).buffer, size);
|
||||
}
|
||||
|
||||
export function* chunkArrayBuffer(
|
||||
value: ArrayBufferLike,
|
||||
size: number
|
||||
): Generator<ArrayBuffer, void, void> {
|
||||
if (value.byteLength <= size) {
|
||||
return yield value;
|
||||
}
|
||||
|
||||
for (let i = 0; i < value.byteLength; i += size) {
|
||||
yield value.slice(i, i + size);
|
||||
}
|
||||
}
|
||||
|
||||
export async function* chunkAsyncIterable(
|
||||
value: AsyncIterable<ArrayBuffer>,
|
||||
size: number
|
||||
): AsyncGenerator<ArrayBuffer, void, void> {
|
||||
let result = new Uint8Array(size);
|
||||
let index = 0;
|
||||
for await (let buffer of value) {
|
||||
// `result` has some data, `result + buffer` is enough
|
||||
if (index !== 0 && index + buffer.byteLength >= size) {
|
||||
const remainder = size - index;
|
||||
result.set(new Uint8Array(buffer, 0, remainder), index);
|
||||
yield result.buffer;
|
||||
|
||||
result = new Uint8Array(size);
|
||||
index = 0;
|
||||
|
||||
if (buffer.byteLength > remainder) {
|
||||
// `buffer` still has some data
|
||||
buffer = buffer.slice(remainder);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// `result` is empty, `buffer` alone is enough
|
||||
if (buffer.byteLength >= size) {
|
||||
let remainder = false;
|
||||
for (const chunk of chunkArrayBuffer(buffer, size)) {
|
||||
if (chunk.byteLength === size) {
|
||||
yield chunk;
|
||||
}
|
||||
|
||||
// `buffer` still has some data
|
||||
remainder = true;
|
||||
buffer = chunk;
|
||||
}
|
||||
|
||||
if (!remainder) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// `result` has some data but `result + buffer` is still not enough
|
||||
// or after previous steps `buffer` still has some data
|
||||
result.set(new Uint8Array(buffer), index);
|
||||
index += buffer.byteLength;
|
||||
}
|
||||
}
|
||||
import { AdbSyncEntryResponse, adbSyncOpenDir } from './list';
|
||||
import { adbSyncPull } from './receive';
|
||||
import { adbSyncPush } from './send';
|
||||
import { adbSyncLstat, adbSyncStat } from './stat';
|
||||
|
||||
export class AdbSync extends AutoDisposable {
|
||||
protected adb: Adb;
|
||||
|
@ -274,37 +26,11 @@ export class AdbSync extends AutoDisposable {
|
|||
this.stream = new AdbBufferedStream(stream);
|
||||
}
|
||||
|
||||
protected send<T extends Struct<object, object, object, unknown>>(
|
||||
type: T,
|
||||
value: StructInitType<T>
|
||||
) {
|
||||
return this.stream.write(type.serialize(value, this.stream.backend));
|
||||
}
|
||||
|
||||
public async lstat(path: string): Promise<AdbSyncLstatResponse | AdbSyncStatResponse> {
|
||||
public async lstat(path: string) {
|
||||
await this.sendLock.wait();
|
||||
|
||||
try {
|
||||
let requestId: AdbSyncRequestId.Lstat | AdbSyncRequestId.Lstat2;
|
||||
let responseType: typeof AdbSyncLstatResponse | typeof AdbSyncStatResponse;
|
||||
let responseId: AdbSyncResponseId.Lstat | AdbSyncResponseId.Stat;
|
||||
|
||||
if (this.supportStat) {
|
||||
requestId = AdbSyncRequestId.Lstat2;
|
||||
responseType = AdbSyncStatResponse;
|
||||
responseId = AdbSyncResponseId.Stat;
|
||||
} else {
|
||||
requestId = AdbSyncRequestId.Lstat;
|
||||
responseType = AdbSyncLstatResponse;
|
||||
responseId = AdbSyncResponseId.Lstat;
|
||||
}
|
||||
|
||||
await this.send(AdbSyncStringRequest, { id: requestId, data: path });
|
||||
const response = await readResponse(this.stream, responseType.size);
|
||||
if (response.id !== responseId) {
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
||||
return response;
|
||||
return adbSyncLstat(this.stream, path, this.supportStat);
|
||||
} finally {
|
||||
this.sendLock.notify();
|
||||
}
|
||||
|
@ -318,48 +44,28 @@ export class AdbSync extends AutoDisposable {
|
|||
await this.sendLock.wait();
|
||||
|
||||
try {
|
||||
await this.send(AdbSyncStringRequest, { id: AdbSyncRequestId.Stat, data: path });
|
||||
const response = await readResponse(this.stream, AdbSyncStatResponse.size);
|
||||
if (response.id !== AdbSyncResponseId.Stat) {
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
||||
return response;
|
||||
return adbSyncStat(this.stream, path);
|
||||
} finally {
|
||||
this.sendLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
public async isDirectory(path: string): Promise<boolean> {
|
||||
await this.sendLock.wait();
|
||||
|
||||
try {
|
||||
await this.stat(path + '/');
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
this.sendLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
public async *opendir(path: string) {
|
||||
public async *opendir(
|
||||
path: string
|
||||
): AsyncGenerator<AdbSyncEntryResponse, void, void> {
|
||||
await this.sendLock.wait();
|
||||
|
||||
try {
|
||||
await this.send(AdbSyncStringRequest, { id: AdbSyncRequestId.List, data: path });
|
||||
|
||||
while (true) {
|
||||
const response = await readResponse(this.stream, AdbSyncEntryResponse.size);
|
||||
switch (response.id) {
|
||||
case AdbSyncResponseId.Entry:
|
||||
yield response;
|
||||
break;
|
||||
case AdbSyncResponseId.Done:
|
||||
return;
|
||||
default:
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
||||
}
|
||||
yield* adbSyncOpenDir(this.stream, path);
|
||||
} finally {
|
||||
this.sendLock.notify();
|
||||
}
|
||||
|
@ -377,36 +83,12 @@ export class AdbSync extends AutoDisposable {
|
|||
await this.sendLock.wait();
|
||||
|
||||
try {
|
||||
await this.send(AdbSyncStringRequest, { id: AdbSyncRequestId.Receive, data: path });
|
||||
while (true) {
|
||||
const response = await readResponse(this.stream, AdbSyncDataResponse.size);
|
||||
switch (response.id) {
|
||||
case AdbSyncResponseId.Data:
|
||||
yield response.data!;
|
||||
break;
|
||||
case AdbSyncResponseId.Done:
|
||||
return;
|
||||
default:
|
||||
throw new Error('Unexpected response id');
|
||||
}
|
||||
}
|
||||
yield* adbSyncPull(this.stream, path);
|
||||
} finally {
|
||||
this.sendLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
public async write(
|
||||
path: string,
|
||||
file: AsyncIterable<ArrayBuffer>,
|
||||
mode?: number,
|
||||
mtime?: number,
|
||||
): Promise<void>;
|
||||
public async write(
|
||||
path: string,
|
||||
file: ArrayLike<number>,
|
||||
mode?: number,
|
||||
mtime?: number,
|
||||
): Promise<void>;
|
||||
public async write(
|
||||
path: string,
|
||||
file: AsyncIterable<ArrayBuffer> | ArrayLike<number>,
|
||||
|
@ -414,35 +96,9 @@ export class AdbSync extends AutoDisposable {
|
|||
mtime = Date.now(),
|
||||
): Promise<void> {
|
||||
await this.sendLock.wait();
|
||||
const packetSize = 64 * 1024;
|
||||
|
||||
try {
|
||||
const pathAndMode = `${path},${mode.toString(8)}`;
|
||||
await this.send(AdbSyncStringRequest, {
|
||||
id: AdbSyncRequestId.Send,
|
||||
data: pathAndMode
|
||||
});
|
||||
|
||||
let chunkReader: Iterable<ArrayBuffer> | AsyncIterable<ArrayBuffer>;
|
||||
if ('length' in file) {
|
||||
chunkReader = chunkArray(file, packetSize);
|
||||
} else if ('byteLength' in file) {
|
||||
chunkReader = chunkArrayBuffer(file, packetSize);
|
||||
} else {
|
||||
chunkReader = chunkAsyncIterable(file, packetSize);
|
||||
}
|
||||
|
||||
for await (const buffer of chunkReader) {
|
||||
await this.send(AdbSyncBufferRequest, {
|
||||
id: AdbSyncRequestId.Data,
|
||||
data: buffer,
|
||||
});
|
||||
}
|
||||
|
||||
await this.send(AdbSyncNumberRequest, {
|
||||
id: AdbSyncRequestId.Send,
|
||||
arg: mtime
|
||||
});
|
||||
adbSyncPush(this.stream, path, file, mode, mtime);
|
||||
} finally {
|
||||
this.sendLock.notify();
|
||||
}
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
import { AdbCommandBase } from './base';
|
||||
|
||||
export class AdbTcpIpCommand extends AdbCommandBase {
|
||||
private async getProp(key: string): Promise<string> {
|
||||
const output = await this.adb.shell('getprop', key);
|
||||
return output.trim();
|
||||
}
|
||||
|
||||
public async getAddresses(): Promise<string[]> {
|
||||
const propAddr = await this.getProp('service.adb.listen_addrs');
|
||||
const propAddr = await this.adb.getProp('service.adb.listen_addrs');
|
||||
if (propAddr) {
|
||||
return propAddr.split(',');
|
||||
}
|
||||
|
||||
let port = await this.getProp('service.adb.tcp.port');
|
||||
let port = await this.adb.getProp('service.adb.tcp.port');
|
||||
if (port) {
|
||||
return [`0.0.0.0:${port}`];
|
||||
}
|
||||
|
||||
port = await this.getProp('persist.adb.tcp.port');
|
||||
port = await this.adb.getProp('persist.adb.tcp.port');
|
||||
if (port) {
|
||||
return [`0.0.0.0:${port}`];
|
||||
}
|
||||
|
|
|
@ -257,7 +257,7 @@ export function sign(privateKey: ArrayBuffer, data: ArrayBuffer): ArrayBuffer {
|
|||
const fillLength = padded.length - Sha1DigestInfo.length - data.byteLength - 1;
|
||||
while (index < fillLength) {
|
||||
padded[index] = 0xff;
|
||||
index++;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
padded[index] = 0;
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
export * from './adb';
|
||||
export * from './auth';
|
||||
export * from './backend';
|
||||
export * from './commands';
|
||||
export * from './crypto';
|
||||
export * from './features';
|
||||
export * from './framebuffer';
|
||||
export * from './packet';
|
||||
export * from './stream';
|
||||
export * from './sync';
|
||||
export * from './utils';
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface AdbPacketReceivedEventArgs {
|
|||
packet: AdbPacket;
|
||||
}
|
||||
|
||||
export interface AdbStreamCreatedEventArgs {
|
||||
export interface AdbIncomingStreamEventArgs {
|
||||
handled: boolean;
|
||||
|
||||
packet: AdbPacket;
|
||||
|
@ -30,7 +30,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
private readonly packetEvent = this.addDisposable(new EventEmitter<AdbPacketReceivedEventArgs>());
|
||||
public get onPacket() { return this.packetEvent.event; }
|
||||
|
||||
private readonly streamEvent = this.addDisposable(new EventEmitter<AdbStreamCreatedEventArgs>());
|
||||
private readonly streamEvent = this.addDisposable(new EventEmitter<AdbIncomingStreamEventArgs>());
|
||||
public get onStream() { return this.streamEvent.event; }
|
||||
|
||||
private readonly errorEvent = this.addDisposable(new EventEmitter<Error>());
|
||||
|
@ -132,7 +132,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
const controller = new AdbStreamController(localId, remoteId, this);
|
||||
const stream = new AdbStream(controller);
|
||||
|
||||
const args: AdbStreamCreatedEventArgs = {
|
||||
const args: AdbIncomingStreamEventArgs = {
|
||||
handled: false,
|
||||
packet,
|
||||
stream,
|
||||
|
|
|
@ -18,7 +18,7 @@ function addRange(start: string, end: string) {
|
|||
const endCharCode = end.charCodeAt(0);
|
||||
const length = endCharCode - startCharCode + 1;
|
||||
|
||||
for (let i = startCharCode; i <= endCharCode; i++) {
|
||||
for (let i = startCharCode; i <= endCharCode; i += 1) {
|
||||
chars.push(i);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Array, FieldDescriptorBase, FieldDescriptorBaseOptions, FieldType, Fiel
|
|||
import { StructDefaultOptions, StructDeserializationContext, StructOptions, StructSerializationContext } from './types';
|
||||
import { Evaluate, Identity, OmitNever, Overwrite } from './utils';
|
||||
|
||||
export type StructValueType<T extends Struct<object, object, object, unknown>> =
|
||||
T extends { deserialize(reader: StructDeserializationContext): Promise<infer R>; } ? R : never;
|
||||
export type StructValueType<T> =
|
||||
T extends { deserialize(context: StructDeserializationContext): Promise<infer R>; } ? R : never;
|
||||
|
||||
export type StructInitType<T extends Struct<object, object, object, unknown>> =
|
||||
T extends { create(value: infer R, ...args: any): any; } ? Evaluate<R> : never;
|
||||
|
@ -407,7 +407,7 @@ export default class Struct<
|
|||
|
||||
let size = this._size;
|
||||
let fieldSize: number[] = [];
|
||||
for (let i = 0; i < this.fields.length; i++) {
|
||||
for (let i = 0; i < this.fields.length; i += 1) {
|
||||
const field = this.fields[i];
|
||||
const type = getFieldTypeDefinition(field.type);
|
||||
if (type.getDynamicSize) {
|
||||
|
@ -426,7 +426,7 @@ export default class Struct<
|
|||
const buffer = new ArrayBuffer(size);
|
||||
const dataView = new DataView(buffer);
|
||||
let offset = 0;
|
||||
for (let i = 0; i < this.fields.length; i++) {
|
||||
for (let i = 0; i < this.fields.length; i += 1) {
|
||||
const field = this.fields[i];
|
||||
const type = getFieldTypeDefinition(field.type);
|
||||
type.serialize({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue