Home | History | Annotate | Download | only in util
      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 
     43     public ProcFileReader(InputStream stream) throws IOException {
     44         this(stream, 4096);
     45     }
     46 
     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      */
     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      */
     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      */
     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      */
    118     public boolean hasMoreData() {
    119         return mTail > 0;
    120     }
    121 
    122     /**
    123      * Finish current line, skipping any remaining data.
    124      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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      */
    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
    230     public void close() throws IOException {
    231         mStream.close();
    232     }
    233 }
    234