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