1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {assertDefined} from 'common/assert_utils';
18import {Timestamp} from 'common/time';
19import {AbstractParser} from 'parsers/legacy/abstract_parser';
20import {LogMessage} from 'parsers/protolog/log_message';
21import {ParserProtologUtils} from 'parsers/protolog/parser_protolog_utils';
22import root from 'protos/protolog/udc/json';
23import {com} from 'protos/protolog/udc/static';
24import {TraceType} from 'trace/trace_type';
25import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
26import configJson32 from '../../../../configs/services.core.protolog32.json';
27import configJson64 from '../../../../configs/services.core.protolog64.json';
28
29class ParserProtoLog extends AbstractParser {
30  private static readonly ProtoLogFileProto = root.lookupType(
31    'com.android.internal.protolog.ProtoLogFileProto',
32  );
33  private static readonly MAGIC_NUMBER = [
34    0x09, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x4c, 0x4f, 0x47,
35  ]; // .PROTOLOG
36  private static readonly PROTOLOG_32_BIT_VERSION = '1.0.0';
37  private static readonly PROTOLOG_64_BIT_VERSION = '2.0.0';
38
39  private realToBootTimeOffsetNs: bigint | undefined;
40
41  override getTraceType(): TraceType {
42    return TraceType.PROTO_LOG;
43  }
44
45  override getMagicNumber(): number[] {
46    return ParserProtoLog.MAGIC_NUMBER;
47  }
48
49  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
50    return undefined;
51  }
52
53  override getRealToBootTimeOffsetNs(): bigint | undefined {
54    return this.realToBootTimeOffsetNs;
55  }
56
57  override decodeTrace(
58    buffer: Uint8Array,
59  ): com.android.internal.protolog.IProtoLogMessage[] {
60    const fileProto = ParserProtoLog.ProtoLogFileProto.decode(
61      buffer,
62    ) as com.android.internal.protolog.IProtoLogFileProto;
63
64    if (fileProto.version === ParserProtoLog.PROTOLOG_32_BIT_VERSION) {
65      if (configJson32.version !== ParserProtoLog.PROTOLOG_32_BIT_VERSION) {
66        const message = `Unsupported ProtoLog JSON config version ${configJson32.version} expected ${ParserProtoLog.PROTOLOG_32_BIT_VERSION}`;
67        console.log(message);
68        throw new TypeError(message);
69      }
70    } else if (fileProto.version === ParserProtoLog.PROTOLOG_64_BIT_VERSION) {
71      if (configJson64.version !== ParserProtoLog.PROTOLOG_64_BIT_VERSION) {
72        const message = `Unsupported ProtoLog JSON config version ${configJson64.version} expected ${ParserProtoLog.PROTOLOG_64_BIT_VERSION}`;
73        console.log(message);
74        throw new TypeError(message);
75      }
76    } else {
77      const message = 'Unsupported ProtoLog trace version';
78      console.log(message);
79      throw new TypeError(message);
80    }
81
82    this.realToBootTimeOffsetNs =
83      BigInt(
84        assertDefined(fileProto.realTimeToElapsedTimeOffsetMillis).toString(),
85      ) * 1000000n;
86
87    if (!fileProto.log) {
88      return [];
89    }
90
91    fileProto.log.sort(
92      (
93        a: com.android.internal.protolog.IProtoLogMessage,
94        b: com.android.internal.protolog.IProtoLogMessage,
95      ) => {
96        return Number(a.elapsedRealtimeNanos) - Number(b.elapsedRealtimeNanos);
97      },
98    );
99
100    return fileProto.log;
101  }
102
103  protected override getTimestamp(
104    entry: com.android.internal.protolog.IProtoLogMessage,
105  ): Timestamp {
106    return this.timestampConverter.makeTimestampFromBootTimeNs(
107      BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
108    );
109  }
110
111  override processDecodedEntry(
112    index: number,
113    entry: com.android.internal.protolog.IProtoLogMessage,
114  ): PropertyTreeNode {
115    let messageHash = assertDefined(entry.messageHash).toString();
116    let config: ProtologConfig | undefined = undefined;
117    if (messageHash !== null && messageHash !== '0') {
118      config = assertDefined(configJson64) as ProtologConfig;
119    } else {
120      messageHash = assertDefined(entry.messageHashLegacy).toString();
121      config = assertDefined(configJson32) as ProtologConfig;
122    }
123
124    const message: ConfigMessage | undefined = config.messages[messageHash];
125    const tag: string | undefined = message
126      ? config.groups[message.group].tag
127      : undefined;
128
129    const logMessage = this.makeLogMessage(entry, message, tag);
130    return ParserProtologUtils.makeMessagePropertiesTree(
131      logMessage,
132      this.timestampConverter,
133      this.getRealToMonotonicTimeOffsetNs() !== undefined,
134    );
135  }
136
137  private makeLogMessage(
138    entry: com.android.internal.protolog.IProtoLogMessage,
139    message: ConfigMessage | undefined,
140    tag: string | undefined,
141  ): LogMessage {
142    if (!message || !tag) {
143      return this.makeLogMessageWithoutFormat(entry);
144    }
145    try {
146      return this.makeLogMessageWithFormat(entry, message, tag);
147    } catch (error) {
148      if (error instanceof FormatStringMismatchError) {
149        return this.makeLogMessageWithoutFormat(entry);
150      }
151      throw error;
152    }
153  }
154
155  private makeLogMessageWithFormat(
156    entry: com.android.internal.protolog.IProtoLogMessage,
157    message: ConfigMessage,
158    tag: string,
159  ): LogMessage {
160    let text = '';
161
162    const strParams: string[] = assertDefined(entry.strParams);
163    let strParamsIdx = 0;
164    const sint64Params: Array<bigint> = assertDefined(entry.sint64Params).map(
165      (param) => BigInt(param.toString()),
166    );
167    let sint64ParamsIdx = 0;
168    const doubleParams: number[] = assertDefined(entry.doubleParams);
169    let doubleParamsIdx = 0;
170    const booleanParams: boolean[] = assertDefined(entry.booleanParams);
171    let booleanParamsIdx = 0;
172
173    const messageFormat = message.message;
174    for (let i = 0; i < messageFormat.length; ) {
175      if (messageFormat[i] === '%') {
176        if (i + 1 >= messageFormat.length) {
177          // Should never happen - protologtool checks for that
178          throw new Error('Invalid format string');
179        }
180        switch (messageFormat[i + 1]) {
181          case '%':
182            text += '%';
183            break;
184          case 'd':
185            text += this.getParam(sint64Params, sint64ParamsIdx++).toString(10);
186            break;
187          case 'o':
188            text += this.getParam(sint64Params, sint64ParamsIdx++).toString(8);
189            break;
190          case 'x':
191            text += this.getParam(sint64Params, sint64ParamsIdx++).toString(16);
192            break;
193          case 'f':
194            text += this.getParam(doubleParams, doubleParamsIdx++).toFixed(6);
195            break;
196          case 'e':
197            text += this.getParam(
198              doubleParams,
199              doubleParamsIdx++,
200            ).toExponential();
201            break;
202          case 'g':
203            text += this.getParam(doubleParams, doubleParamsIdx++).toString();
204            break;
205          case 's':
206            text += this.getParam(strParams, strParamsIdx++);
207            break;
208          case 'b':
209            text += this.getParam(booleanParams, booleanParamsIdx++).toString();
210            break;
211          default:
212            // Should never happen - protologtool checks for that
213            throw new Error(
214              'Invalid format string conversion: ' + messageFormat[i + 1],
215            );
216        }
217        i += 2;
218      } else {
219        text += messageFormat[i];
220        i += 1;
221      }
222    }
223
224    return {
225      text,
226      tag,
227      level: message.level,
228      at: message.at,
229      timestamp: BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
230    };
231  }
232
233  private getParam<T>(arr: T[], idx: number): T {
234    if (arr.length <= idx) {
235      throw new Error('No param for format string conversion');
236    }
237    return arr[idx];
238  }
239
240  private makeLogMessageWithoutFormat(
241    entry: com.android.internal.protolog.IProtoLogMessage,
242  ): LogMessage {
243    const text =
244      assertDefined(entry.messageHash).toString() +
245      ' - [' +
246      assertDefined(entry.strParams).toString() +
247      '] [' +
248      assertDefined(entry.sint64Params).toString() +
249      '] [' +
250      assertDefined(entry.doubleParams).toString() +
251      '] [' +
252      assertDefined(entry.booleanParams).toString() +
253      ']';
254
255    return {
256      text,
257      tag: 'INVALID',
258      level: 'invalid',
259      at: '',
260      timestamp: BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
261    };
262  }
263}
264
265class FormatStringMismatchError extends Error {
266  constructor(message: string) {
267    super(message);
268  }
269}
270
271interface ProtologConfig {
272  version: string;
273  messages: {[key: string]: ConfigMessage};
274  groups: {[key: string]: {tag: string}};
275}
276
277interface ConfigMessage {
278  message: string;
279  level: string;
280  group: string;
281  at: string;
282}
283
284export {ParserProtoLog};
285