1/*
2 * Copyright (C) 2023 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 {FileUtils} from 'common/file_utils';
19import {TimezoneInfo} from 'common/time';
20import {UserNotificationsListener} from 'messaging/user_notifications_listener';
21import {TraceOverridden} from 'messaging/user_warnings';
22import {TraceFile} from 'trace/trace_file';
23
24export interface FilterResult {
25  legacy: TraceFile[];
26  perfetto?: TraceFile;
27  timezoneInfo?: TimezoneInfo;
28}
29
30export class TraceFileFilter {
31  private static readonly BUGREPORT_SYSTRACE_PATH =
32    'FS/data/misc/perfetto-traces/bugreport/systrace.pftrace';
33  private static readonly BUGREPORT_LEGACY_FILES_ALLOWLIST = [
34    'FS/data/misc/wmtrace/',
35    'FS/data/misc/perfetto-traces/',
36    'proto/window_CRITICAL.proto',
37    'proto/input_method_CRITICAL.proto',
38    'proto/SurfaceFlinger_CRITICAL.proto',
39  ];
40  private static readonly PERFETTO_EXTENSIONS = [
41    '.pftrace',
42    '.perfetto-trace',
43    '.perfetto',
44  ];
45
46  async filter(
47    files: TraceFile[],
48    UserNotificationsListener: UserNotificationsListener,
49  ): Promise<FilterResult> {
50    const bugreportMainEntry = files.find((file) =>
51      file.file.name.endsWith('main_entry.txt'),
52    );
53
54    const perfettoFiles = files.filter((file) => this.isPerfettoFile(file));
55    const legacyFiles = files.filter((file) => !this.isPerfettoFile(file));
56    if (!(await this.isBugreport(bugreportMainEntry, files))) {
57      const perfettoFile = this.pickLargestFile(
58        perfettoFiles,
59        UserNotificationsListener,
60      );
61      return {
62        perfetto: perfettoFile,
63        legacy: legacyFiles,
64      };
65    }
66
67    const timezoneInfo = await this.processRawBugReport(
68      assertDefined(bugreportMainEntry),
69      files,
70    );
71
72    return await this.filterBugreport(
73      assertDefined(bugreportMainEntry),
74      perfettoFiles,
75      legacyFiles,
76      timezoneInfo,
77    );
78  }
79
80  private async processRawBugReport(
81    bugreportMainEntry: TraceFile,
82    files: TraceFile[],
83  ): Promise<TimezoneInfo | undefined> {
84    const bugreportName = (await bugreportMainEntry.file.text()).trim();
85    const rawBugReport = files.find((file) => file.file.name === bugreportName);
86    if (!rawBugReport) {
87      return undefined;
88    }
89
90    const traceBuffer = new Uint8Array(await rawBugReport.file.arrayBuffer());
91    const fileData = new TextDecoder().decode(traceBuffer);
92
93    const timezoneStartIndex = fileData.indexOf('[persist.sys.timezone]');
94    if (timezoneStartIndex === -1) {
95      return undefined;
96    }
97    const timezone = this.extractValueFromRawBugReport(
98      fileData,
99      timezoneStartIndex,
100    );
101
102    return {timezone, locale: 'en-US'};
103  }
104
105  private extractValueFromRawBugReport(
106    fileData: string,
107    startIndex: number,
108  ): string {
109    return fileData
110      .slice(startIndex)
111      .split(']', 2)
112      .map((substr) => {
113        const start = substr.lastIndexOf('[');
114        return substr.slice(start + 1);
115      })[1];
116  }
117
118  private async isBugreport(
119    bugreportMainEntry: TraceFile | undefined,
120    files: TraceFile[],
121  ): Promise<boolean> {
122    if (!bugreportMainEntry) {
123      return false;
124    }
125    const bugreportName = (await bugreportMainEntry.file.text()).trim();
126    return (
127      files.find((file) => {
128        return (
129          file.parentArchive === bugreportMainEntry.parentArchive &&
130          file.file.name === bugreportName
131        );
132      }) !== undefined
133    );
134  }
135
136  private async filterBugreport(
137    bugreportMainEntry: TraceFile,
138    perfettoFiles: TraceFile[],
139    legacyFiles: TraceFile[],
140    timezoneInfo?: TimezoneInfo,
141  ): Promise<FilterResult> {
142    const isFileAllowlisted = (file: TraceFile) => {
143      for (const traceDir of TraceFileFilter.BUGREPORT_LEGACY_FILES_ALLOWLIST) {
144        if (file.file.name.startsWith(traceDir)) {
145          return true;
146        }
147      }
148      return false;
149    };
150
151    const fileBelongsToBugreport = (file: TraceFile) =>
152      file.parentArchive === bugreportMainEntry.parentArchive;
153
154    legacyFiles = legacyFiles.filter((file) => {
155      return isFileAllowlisted(file) || !fileBelongsToBugreport(file);
156    });
157
158    const unzippedLegacyFiles: TraceFile[] = [];
159
160    for (const file of legacyFiles) {
161      if (await FileUtils.isZipFile(file.file)) {
162        try {
163          const subFiles = await FileUtils.unzipFile(file.file);
164          const subTraceFiles = subFiles.map((subFile) => {
165            return new TraceFile(subFile, file.file);
166          });
167          unzippedLegacyFiles.push(...subTraceFiles);
168        } catch {
169          unzippedLegacyFiles.push(file);
170        }
171      } else {
172        unzippedLegacyFiles.push(file);
173      }
174    }
175    const perfettoFile = perfettoFiles.find(
176      (file) => file.file.name === TraceFileFilter.BUGREPORT_SYSTRACE_PATH,
177    );
178    return {perfetto: perfettoFile, legacy: unzippedLegacyFiles, timezoneInfo};
179  }
180
181  private isPerfettoFile(file: TraceFile): boolean {
182    return TraceFileFilter.PERFETTO_EXTENSIONS.some((perfettoExt) => {
183      return (
184        file.file.name.endsWith(perfettoExt) ||
185        file.file.name.endsWith(`${perfettoExt}.gz`)
186      );
187    });
188  }
189
190  private pickLargestFile(
191    files: TraceFile[],
192    UserNotificationsListener: UserNotificationsListener,
193  ): TraceFile | undefined {
194    if (files.length === 0) {
195      return undefined;
196    }
197    return files.reduce((largestSoFar, file) => {
198      const [largest, overridden] =
199        largestSoFar.file.size > file.file.size
200          ? [largestSoFar, file]
201          : [file, largestSoFar];
202      UserNotificationsListener.onNotifications([
203        new TraceOverridden(overridden.getDescriptor()),
204      ]);
205      return largest;
206    });
207  }
208}
209