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