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