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.internal.util;
18
19import java.io.Closeable;
20import java.io.IOException;
21import java.io.InputStream;
22import java.net.ProtocolException;
23import java.nio.charset.StandardCharsets;
24
25/**
26 * Reader that specializes in parsing {@code /proc/} files quickly. Walks
27 * through the stream using a single space {@code ' '} as token separator, and
28 * requires each line boundary to be explicitly acknowledged using
29 * {@link #finishLine()}. Assumes {@link StandardCharsets#US_ASCII} encoding.
30 * <p>
31 * Currently doesn't support formats based on {@code \0}, tabs, or repeated
32 * delimiters.
33 */
34public class ProcFileReader implements Closeable {
35    private final InputStream mStream;
36    private final byte[] mBuffer;
37
38    /** Write pointer in {@link #mBuffer}. */
39    private int mTail;
40    /** Flag when last read token finished current line. */
41    private boolean mLineFinished;
42
43    public ProcFileReader(InputStream stream) throws IOException {
44        this(stream, 4096);
45    }
46
47    public ProcFileReader(InputStream stream, int bufferSize) throws IOException {
48        mStream = stream;
49        mBuffer = new byte[bufferSize];
50
51        // read enough to answer hasMoreData
52        fillBuf();
53    }
54
55    /**
56     * Read more data from {@link #mStream} into internal buffer.
57     */
58    private int fillBuf() throws IOException {
59        final int length = mBuffer.length - mTail;
60        if (length == 0) {
61            throw new IOException("attempting to fill already-full buffer");
62        }
63
64        final int read = mStream.read(mBuffer, mTail, length);
65        if (read != -1) {
66            mTail += read;
67        }
68        return read;
69    }
70
71    /**
72     * Consume number of bytes from beginning of internal buffer. If consuming
73     * all remaining bytes, will attempt to {@link #fillBuf()}.
74     */
75    private void consumeBuf(int count) throws IOException {
76        // TODO: consider moving to read pointer, but for now traceview says
77        // these copies aren't a bottleneck.
78        System.arraycopy(mBuffer, count, mBuffer, 0, mTail - count);
79        mTail -= count;
80        if (mTail == 0) {
81            fillBuf();
82        }
83    }
84
85    /**
86     * Find buffer index of next token delimiter, usually space or newline.
87     * Fills buffer as needed.
88     *
89     * @return Index of next delimeter, otherwise -1 if no tokens remain on
90     *         current line.
91     */
92    private int nextTokenIndex() throws IOException {
93        if (mLineFinished) {
94            return -1;
95        }
96
97        int i = 0;
98        do {
99            // scan forward for token boundary
100            for (; i < mTail; i++) {
101                final byte b = mBuffer[i];
102                if (b == '\n') {
103                    mLineFinished = true;
104                    return i;
105                }
106                if (b == ' ') {
107                    return i;
108                }
109            }
110        } while (fillBuf() > 0);
111
112        throw new ProtocolException("End of stream while looking for token boundary");
113    }
114
115    /**
116     * Check if stream has more data to be parsed.
117     */
118    public boolean hasMoreData() {
119        return mTail > 0;
120    }
121
122    /**
123     * Finish current line, skipping any remaining data.
124     */
125    public void finishLine() throws IOException {
126        // last token already finished line; reset silently
127        if (mLineFinished) {
128            mLineFinished = false;
129            return;
130        }
131
132        int i = 0;
133        do {
134            // scan forward for line boundary and consume
135            for (; i < mTail; i++) {
136                if (mBuffer[i] == '\n') {
137                    consumeBuf(i + 1);
138                    return;
139                }
140            }
141        } while (fillBuf() > 0);
142
143        throw new ProtocolException("End of stream while looking for line boundary");
144    }
145
146    /**
147     * Parse and return next token as {@link String}.
148     */
149    public String nextString() throws IOException {
150        final int tokenIndex = nextTokenIndex();
151        if (tokenIndex == -1) {
152            throw new ProtocolException("Missing required string");
153        } else {
154            return parseAndConsumeString(tokenIndex);
155        }
156    }
157
158    /**
159     * Parse and return next token as base-10 encoded {@code long}.
160     */
161    public long nextLong() throws IOException {
162        final int tokenIndex = nextTokenIndex();
163        if (tokenIndex == -1) {
164            throw new ProtocolException("Missing required long");
165        } else {
166            return parseAndConsumeLong(tokenIndex);
167        }
168    }
169
170    /**
171     * Parse and return next token as base-10 encoded {@code long}, or return
172     * the given default value if no remaining tokens on current line.
173     */
174    public long nextOptionalLong(long def) throws IOException {
175        final int tokenIndex = nextTokenIndex();
176        if (tokenIndex == -1) {
177            return def;
178        } else {
179            return parseAndConsumeLong(tokenIndex);
180        }
181    }
182
183    private String parseAndConsumeString(int tokenIndex) throws IOException {
184        final String s = new String(mBuffer, 0, tokenIndex, StandardCharsets.US_ASCII);
185        consumeBuf(tokenIndex + 1);
186        return s;
187    }
188
189    private long parseAndConsumeLong(int tokenIndex) throws IOException {
190        final boolean negative = mBuffer[0] == '-';
191
192        // TODO: refactor into something like IntegralToString
193        long result = 0;
194        for (int i = negative ? 1 : 0; i < tokenIndex; i++) {
195            final int digit = mBuffer[i] - '0';
196            if (digit < 0 || digit > 9) {
197                throw invalidLong(tokenIndex);
198            }
199
200            // always parse as negative number and apply sign later; this
201            // correctly handles MIN_VALUE which is "larger" than MAX_VALUE.
202            final long next = result * 10 - digit;
203            if (next > result) {
204                throw invalidLong(tokenIndex);
205            }
206            result = next;
207        }
208
209        consumeBuf(tokenIndex + 1);
210        return negative ? result : -result;
211    }
212
213    private NumberFormatException invalidLong(int tokenIndex) {
214        return new NumberFormatException(
215                "invalid long: " + new String(mBuffer, 0, tokenIndex, StandardCharsets.US_ASCII));
216    }
217
218    /**
219     * Parse and return next token as base-10 encoded {@code int}.
220     */
221    public int nextInt() throws IOException {
222        final long value = nextLong();
223        if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) {
224            throw new NumberFormatException("parsed value larger than integer");
225        }
226        return (int) value;
227    }
228
229    @Override
230    public void close() throws IOException {
231        mStream.close();
232    }
233}
234