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