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.IOException;
21 import java.io.InputStream;
22 import java.net.ProtocolException;
23 import 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  */
34 public 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 
ProcFileReader(InputStream stream)43     public ProcFileReader(InputStream stream) throws IOException {
44         this(stream, 4096);
45     }
46 
ProcFileReader(InputStream stream, int bufferSize)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      */
fillBuf()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      */
consumeBuf(int count)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      */
nextTokenIndex()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      */
hasMoreData()118     public boolean hasMoreData() {
119         return mTail > 0;
120     }
121 
122     /**
123      * Finish current line, skipping any remaining data.
124      */
finishLine()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      */
nextString()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      */
nextLong()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      */
nextOptionalLong(long def)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 
parseAndConsumeString(int tokenIndex)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 
parseAndConsumeLong(int tokenIndex)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 
invalidLong(int tokenIndex)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      */
nextInt()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
close()230     public void close() throws IOException {
231         mStream.close();
232     }
233 }
234