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