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. 32 * Consecutive spaces are treated as a single delimiter. 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 79 // skip all consecutive delimiters. 80 while (count < mTail && mBuffer[count] == ' ') { 81 count++; 82 } 83 System.arraycopy(mBuffer, count, mBuffer, 0, mTail - count); 84 mTail -= count; 85 if (mTail == 0) { 86 fillBuf(); 87 } 88 } 89 90 /** 91 * Find buffer index of next token delimiter, usually space or newline. 92 * Fills buffer as needed. 93 * 94 * @return Index of next delimeter, otherwise -1 if no tokens remain on 95 * current line. 96 */ nextTokenIndex()97 private int nextTokenIndex() throws IOException { 98 if (mLineFinished) { 99 return -1; 100 } 101 102 int i = 0; 103 do { 104 // scan forward for token boundary 105 for (; i < mTail; i++) { 106 final byte b = mBuffer[i]; 107 if (b == '\n') { 108 mLineFinished = true; 109 return i; 110 } 111 if (b == ' ') { 112 return i; 113 } 114 } 115 } while (fillBuf() > 0); 116 117 throw new ProtocolException("End of stream while looking for token boundary"); 118 } 119 120 /** 121 * Check if stream has more data to be parsed. 122 */ hasMoreData()123 public boolean hasMoreData() { 124 return mTail > 0; 125 } 126 127 /** 128 * Finish current line, skipping any remaining data. 129 */ finishLine()130 public void finishLine() throws IOException { 131 // last token already finished line; reset silently 132 if (mLineFinished) { 133 mLineFinished = false; 134 return; 135 } 136 137 int i = 0; 138 do { 139 // scan forward for line boundary and consume 140 for (; i < mTail; i++) { 141 if (mBuffer[i] == '\n') { 142 consumeBuf(i + 1); 143 return; 144 } 145 } 146 } while (fillBuf() > 0); 147 148 throw new ProtocolException("End of stream while looking for line boundary"); 149 } 150 151 /** 152 * Parse and return next token as {@link String}. 153 */ nextString()154 public String nextString() throws IOException { 155 final int tokenIndex = nextTokenIndex(); 156 if (tokenIndex == -1) { 157 throw new ProtocolException("Missing required string"); 158 } else { 159 return parseAndConsumeString(tokenIndex); 160 } 161 } 162 163 /** 164 * Parse and return next token as base-10 encoded {@code long}. 165 */ nextLong()166 public long nextLong() throws IOException { 167 return nextLong(false); 168 } 169 170 /** 171 * Parse and return next token as base-10 encoded {@code long}. 172 */ nextLong(boolean stopAtInvalid)173 public long nextLong(boolean stopAtInvalid) throws IOException { 174 final int tokenIndex = nextTokenIndex(); 175 if (tokenIndex == -1) { 176 throw new ProtocolException("Missing required long"); 177 } else { 178 return parseAndConsumeLong(tokenIndex, stopAtInvalid); 179 } 180 } 181 182 /** 183 * Parse and return next token as base-10 encoded {@code long}, or return 184 * the given default value if no remaining tokens on current line. 185 */ nextOptionalLong(long def)186 public long nextOptionalLong(long def) throws IOException { 187 final int tokenIndex = nextTokenIndex(); 188 if (tokenIndex == -1) { 189 return def; 190 } else { 191 return parseAndConsumeLong(tokenIndex, false); 192 } 193 } 194 parseAndConsumeString(int tokenIndex)195 private String parseAndConsumeString(int tokenIndex) throws IOException { 196 final String s = new String(mBuffer, 0, tokenIndex, StandardCharsets.US_ASCII); 197 consumeBuf(tokenIndex + 1); 198 return s; 199 } 200 201 /** 202 * If stopAtInvalid is true, don't throw IOException but return whatever parsed so far. 203 */ parseAndConsumeLong(int tokenIndex, boolean stopAtInvalid)204 private long parseAndConsumeLong(int tokenIndex, boolean stopAtInvalid) throws IOException { 205 final boolean negative = mBuffer[0] == '-'; 206 207 // TODO: refactor into something like IntegralToString 208 long result = 0; 209 for (int i = negative ? 1 : 0; i < tokenIndex; i++) { 210 final int digit = mBuffer[i] - '0'; 211 if (digit < 0 || digit > 9) { 212 if (stopAtInvalid) { 213 break; 214 } else { 215 throw invalidLong(tokenIndex); 216 } 217 } 218 219 // always parse as negative number and apply sign later; this 220 // correctly handles MIN_VALUE which is "larger" than MAX_VALUE. 221 final long next = result * 10 - digit; 222 if (next > result) { 223 throw invalidLong(tokenIndex); 224 } 225 result = next; 226 } 227 228 consumeBuf(tokenIndex + 1); 229 return negative ? result : -result; 230 } 231 invalidLong(int tokenIndex)232 private NumberFormatException invalidLong(int tokenIndex) { 233 return new NumberFormatException( 234 "invalid long: " + new String(mBuffer, 0, tokenIndex, StandardCharsets.US_ASCII)); 235 } 236 237 /** 238 * Parse and return next token as base-10 encoded {@code int}. 239 */ nextInt()240 public int nextInt() throws IOException { 241 final long value = nextLong(); 242 if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { 243 throw new NumberFormatException("parsed value larger than integer"); 244 } 245 return (int) value; 246 } 247 248 /** 249 * Bypass the next token. 250 */ nextIgnored()251 public void nextIgnored() throws IOException { 252 final int tokenIndex = nextTokenIndex(); 253 if (tokenIndex == -1) { 254 throw new ProtocolException("Missing required token"); 255 } else { 256 consumeBuf(tokenIndex + 1); 257 } 258 } 259 260 @Override close()261 public void close() throws IOException { 262 mStream.close(); 263 } 264 } 265