1/*
2 * Copyright (C) 2011 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
17package com.android.server;
18
19import android.util.Slog;
20import com.google.android.collect.Lists;
21
22import java.util.ArrayList;
23
24/**
25 * Parsed event from native side of {@link NativeDaemonConnector}.
26 */
27public class NativeDaemonEvent {
28
29    // TODO: keep class ranges in sync with ResponseCode.h
30    // TODO: swap client and server error ranges to roughly mirror HTTP spec
31
32    private final int mCmdNumber;
33    private final int mCode;
34    private final String mMessage;
35    private final String mRawEvent;
36    private final String mLogMessage;
37    private String[] mParsed;
38
39    private NativeDaemonEvent(int cmdNumber, int code, String message,
40                              String rawEvent, String logMessage) {
41        mCmdNumber = cmdNumber;
42        mCode = code;
43        mMessage = message;
44        mRawEvent = rawEvent;
45        mLogMessage = logMessage;
46        mParsed = null;
47    }
48
49    static public final String SENSITIVE_MARKER = "{{sensitive}}";
50
51    public int getCmdNumber() {
52        return mCmdNumber;
53    }
54
55    public int getCode() {
56        return mCode;
57    }
58
59    public String getMessage() {
60        return mMessage;
61    }
62
63    @Deprecated
64    public String getRawEvent() {
65        return mRawEvent;
66    }
67
68    @Override
69    public String toString() {
70        return mLogMessage;
71    }
72
73    /**
74     * Test if event represents a partial response which is continued in
75     * additional subsequent events.
76     */
77    public boolean isClassContinue() {
78        return mCode >= 100 && mCode < 200;
79    }
80
81    /**
82     * Test if event represents a command success.
83     */
84    public boolean isClassOk() {
85        return mCode >= 200 && mCode < 300;
86    }
87
88    /**
89     * Test if event represents a remote native daemon error.
90     */
91    public boolean isClassServerError() {
92        return mCode >= 400 && mCode < 500;
93    }
94
95    /**
96     * Test if event represents a command syntax or argument error.
97     */
98    public boolean isClassClientError() {
99        return mCode >= 500 && mCode < 600;
100    }
101
102    /**
103     * Test if event represents an unsolicited event from native daemon.
104     */
105    public boolean isClassUnsolicited() {
106        return isClassUnsolicited(mCode);
107    }
108
109    private static boolean isClassUnsolicited(int code) {
110        return code >= 600 && code < 700;
111    }
112
113    /**
114     * Verify this event matches the given code.
115     *
116     * @throws IllegalStateException if {@link #getCode()} doesn't match.
117     */
118    public void checkCode(int code) {
119        if (mCode != code) {
120            throw new IllegalStateException("Expected " + code + " but was: " + this);
121        }
122    }
123
124    /**
125     * Parse the given raw event into {@link NativeDaemonEvent} instance.
126     *
127     * @throws IllegalArgumentException when line doesn't match format expected
128     *             from native side.
129     */
130    public static NativeDaemonEvent parseRawEvent(String rawEvent) {
131        final String[] parsed = rawEvent.split(" ");
132        if (parsed.length < 2) {
133            throw new IllegalArgumentException("Insufficient arguments");
134        }
135
136        int skiplength = 0;
137
138        final int code;
139        try {
140            code = Integer.parseInt(parsed[0]);
141            skiplength = parsed[0].length() + 1;
142        } catch (NumberFormatException e) {
143            throw new IllegalArgumentException("problem parsing code", e);
144        }
145
146        int cmdNumber = -1;
147        if (isClassUnsolicited(code) == false) {
148            if (parsed.length < 3) {
149                throw new IllegalArgumentException("Insufficient arguemnts");
150            }
151            try {
152                cmdNumber = Integer.parseInt(parsed[1]);
153                skiplength += parsed[1].length() + 1;
154            } catch (NumberFormatException e) {
155                throw new IllegalArgumentException("problem parsing cmdNumber", e);
156            }
157        }
158
159        String logMessage = rawEvent;
160        if (parsed.length > 2 && parsed[2].equals(SENSITIVE_MARKER)) {
161            skiplength += parsed[2].length() + 1;
162            logMessage = parsed[0] + " " + parsed[1] + " {}";
163        }
164
165        final String message = rawEvent.substring(skiplength);
166
167        return new NativeDaemonEvent(cmdNumber, code, message, rawEvent, logMessage);
168    }
169
170    /**
171     * Filter the given {@link NativeDaemonEvent} list, returning
172     * {@link #getMessage()} for any events matching the requested code.
173     */
174    public static String[] filterMessageList(NativeDaemonEvent[] events, int matchCode) {
175        final ArrayList<String> result = Lists.newArrayList();
176        for (NativeDaemonEvent event : events) {
177            if (event.getCode() == matchCode) {
178                result.add(event.getMessage());
179            }
180        }
181        return result.toArray(new String[result.size()]);
182    }
183
184    /**
185     * Find the Nth field of the event.
186     *
187     * This ignores and code or cmdNum, the first return value is given for N=0.
188     * Also understands "\"quoted\" multiword responses" and tries them as a single field
189     */
190    public String getField(int n) {
191        if (mParsed == null) {
192            mParsed = unescapeArgs(mRawEvent);
193        }
194        n += 2; // skip code and command#
195        if (n > mParsed.length) return null;
196            return mParsed[n];
197        }
198
199    public static String[] unescapeArgs(String rawEvent) {
200        final boolean DEBUG_ROUTINE = false;
201        final String LOGTAG = "unescapeArgs";
202        final ArrayList<String> parsed = new ArrayList<String>();
203        final int length = rawEvent.length();
204        int current = 0;
205        int wordEnd = -1;
206        boolean quoted = false;
207
208        if (DEBUG_ROUTINE) Slog.e(LOGTAG, "parsing '" + rawEvent + "'");
209        if (rawEvent.charAt(current) == '\"') {
210            quoted = true;
211            current++;
212        }
213        while (current < length) {
214            // find the end of the word
215            char terminator = quoted ? '\"' : ' ';
216            wordEnd = current;
217            while (wordEnd < length && rawEvent.charAt(wordEnd) != terminator) {
218                if (rawEvent.charAt(wordEnd) == '\\') {
219                    // skip the escaped char
220                    ++wordEnd;
221                }
222                ++wordEnd;
223            }
224            if (wordEnd > length) wordEnd = length;
225            String word = rawEvent.substring(current, wordEnd);
226            current += word.length();
227            if (!quoted) {
228                word = word.trim();
229            } else {
230                current++;  // skip the trailing quote
231            }
232            // unescape stuff within the word
233            word = word.replace("\\\\", "\\");
234            word = word.replace("\\\"", "\"");
235
236            if (DEBUG_ROUTINE) Slog.e(LOGTAG, "found '" + word + "'");
237            parsed.add(word);
238
239            // find the beginning of the next word - either of these options
240            int nextSpace = rawEvent.indexOf(' ', current);
241            int nextQuote = rawEvent.indexOf(" \"", current);
242            if (DEBUG_ROUTINE) {
243                Slog.e(LOGTAG, "nextSpace=" + nextSpace + ", nextQuote=" + nextQuote);
244            }
245            if (nextQuote > -1 && nextQuote <= nextSpace) {
246                quoted = true;
247                current = nextQuote + 2;
248            } else {
249                quoted = false;
250                if (nextSpace > -1) {
251                    current = nextSpace + 1;
252                }
253            } // else we just start the next word after the current and read til the end
254            if (DEBUG_ROUTINE) {
255                Slog.e(LOGTAG, "next loop - current=" + current +
256                        ", length=" + length + ", quoted=" + quoted);
257            }
258        }
259        return parsed.toArray(new String[parsed.size()]);
260    }
261}
262