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