1// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.archivepatcher.shared;
16
17import java.io.File;
18import java.io.IOException;
19import java.io.InputStream;
20import java.io.RandomAccessFile;
21
22/**
23 * An {@link InputStream} backed by a file that is assumed to be unchanging, such that it is
24 * suitable for random read access. This allows efficient and trivial implementation of the
25 * {@link #mark(int)} and {@link #reset()} functions using file operations.
26 * <p>
27 * This class deliberately breaks from the {@link InputStream} contract because it is intended for
28 * use cases where some of that behavior doesn't make sense. Specifically:
29 * <ul>
30 *  <li>There is no read limit, and the value passed to {@link #mark(int)} is ignored.</li>
31 *  <li>The {@link #reset()} method will only throw an exception if the stream is closed or if
32 *      {@link #mark(int)} has never been called. It does <em>not</em> have the concept of an
33 *      error from an invalidated read limit, because there is no read limit.</li>
34 * </ul>
35 */
36public class RandomAccessFileInputStream extends InputStream {
37  /**
38   * The backing {@link RandomAccessFile}.
39   */
40  private final RandomAccessFile raf;
41
42  /**
43   * The current mark in the file, if set; otherwise -1.
44   */
45  private long mark = -1;
46
47  /**
48   * The offset at which the reading range starts.
49   */
50  private long rangeOffset;
51
52  /**
53   * The number of bytes in the reading range.
54   */
55  private long rangeLength;
56
57  /**
58   * The length of the file at the moment it was opened.
59   */
60  private final long fileLength;
61
62  /**
63   * Constructs a new stream for the given file, which will be opened in read-only mode for random
64   * access cross the entire file. Equivalent to calling
65   * {@link #RandomAccessFileInputStream(File, long, long)} with 0 and {@link File#length()} as the
66   * range parameters.
67   * @param file the file to read
68   * @throws IOException if unable to open the file for read
69   */
70  public RandomAccessFileInputStream(File file) throws IOException {
71    this(file, 0, file.length());
72  }
73
74  /**
75   * Constructs a new stream for the given file, which will be opened in read-only mode for random
76   * access within a specific range.
77   * @param file the file to read
78   * @param rangeOffset the offset at which the valid range starts
79   * @param rangeLength the number of bytes in the range
80   * @throws IOException if unable to open the file for read
81   */
82  public RandomAccessFileInputStream(File file, long rangeOffset, long rangeLength)
83      throws IOException {
84    raf = getRandomAccessFile(file);
85    fileLength = file.length();
86    setRange(rangeOffset, rangeLength);
87  }
88
89  /**
90   * Given a {@link File}, get a read-only {@link RandomAccessFile} reference for it.
91   * @param file the file
92   * @return as described
93   * @throws IOException if unable to open the file
94   */
95  protected RandomAccessFile getRandomAccessFile(File file) throws IOException {
96    return new RandomAccessFile(file, "r");
97  }
98
99  /**
100   * Sets the range to the specified values and seeks to the beginning of that range immediately.
101   * Any previously-existing mark is discarded. Also calls {@link #reset()}.
102   * @param rangeOffset the offset at which the valid range starts, must be a non-negative value
103   * @param rangeLength the number of bytes in the range, must be a non-negative value
104   * @throws IOException if anything goes wrong
105   */
106  public void setRange(long rangeOffset, long rangeLength) throws IOException {
107    if (rangeOffset < 0) {
108      throw new IllegalArgumentException("rangeOffset must be >= 0");
109    }
110    if (rangeLength < 0) {
111      throw new IllegalArgumentException("rangeLength must be >= 0");
112    }
113    if (rangeOffset + rangeLength > fileLength) {
114      throw new IllegalArgumentException("Read range exceeds file length");
115    }
116    if (rangeOffset + rangeLength < 0) {
117      throw new IllegalArgumentException("Insane input size not supported");
118    }
119    this.rangeOffset = rangeOffset;
120    this.rangeLength = rangeLength;
121    mark = rangeOffset;
122    reset();
123    mark = -1;
124  }
125
126  @Override
127  public int available() throws IOException {
128    long rangeRelativePosition = raf.getFilePointer() - rangeOffset;
129    long result = rangeLength - rangeRelativePosition;
130    if (result > Integer.MAX_VALUE) {
131      return Integer.MAX_VALUE;
132    }
133    return (int) result;
134  }
135
136  /**
137   * Returns the current position in the stream.
138   * @return as described
139   * @throws IOException if something goes wrong
140   */
141  public long getPosition() throws IOException {
142    return raf.getFilePointer();
143  }
144
145  @Override
146  public void close() throws IOException {
147    raf.close();
148  }
149
150  @Override
151  public int read() throws IOException {
152    if (available() <= 0) {
153      return -1;
154    }
155    return raf.read();
156  }
157
158  @Override
159  public int read(byte[] b, int off, int len) throws IOException {
160    if (len <= 0) {
161      return 0;
162    }
163    int available = available();
164    if (available <= 0) {
165      return -1;
166    }
167    int result = raf.read(b, off, Math.min(len, available));
168    return result;
169  }
170
171  @Override
172  public int read(byte[] b) throws IOException {
173    return read(b, 0, b.length);
174  }
175
176  @Override
177  public long skip(long n) throws IOException {
178    if (n <= 0) {
179      return 0;
180    }
181    int available = available();
182    if (available <= 0) {
183      return 0;
184    }
185    int skipAmount = (int) Math.min(available, n);
186    raf.seek(raf.getFilePointer() + skipAmount);
187    return skipAmount;
188  }
189
190  @Override
191  public boolean markSupported() {
192    return true;
193  }
194
195  /**
196   * The readlimit argument is ignored for this implementation, as there is no concept of a buffer
197   * to be limited.
198   */
199  @Override
200  public void mark(int readlimit) {
201    try {
202      mark = raf.getFilePointer();
203    } catch (IOException e) {
204      throw new RuntimeException(e);
205    }
206  }
207
208  @Override
209  public void reset() throws IOException {
210    if (mark < 0) {
211      throw new IOException("mark not set");
212    }
213    raf.seek(mark);
214  }
215
216  /**
217   * Returns the total length of the underlying file at the moment it was opened.
218   * @return as described
219   */
220  public long length() {
221    return fileLength;
222  }
223}
224