1 /*
2  * Copyright (C) 2010 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 package com.android.tradefed.util;
17 
18 import com.android.tradefed.result.InputStreamSource;
19 
20 import com.google.common.base.Preconditions;
21 import com.google.common.io.ByteStreams;
22 
23 import java.io.BufferedInputStream;
24 import java.io.BufferedReader;
25 import java.io.ByteArrayOutputStream;
26 import java.io.Closeable;
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InputStreamReader;
32 import java.io.OutputStream;
33 import java.io.PrintStream;
34 import java.io.Reader;
35 import java.io.Writer;
36 import java.security.DigestInputStream;
37 import java.security.MessageDigest;
38 import java.security.NoSuchAlgorithmException;
39 import java.util.Base64;
40 import java.util.Objects;
41 import java.util.zip.CRC32;
42 import java.util.zip.GZIPOutputStream;
43 import java.util.zip.ZipOutputStream;
44 
45 /**
46  * Utility class for managing input streams.
47  */
48 public class StreamUtil {
49 
50     // 16K buffer size
51     private static final int BUF_SIZE = 16 * 1024;
52 
StreamUtil()53     private StreamUtil() {
54     }
55 
56     /**
57      * Retrieves a {@link String} from an {@link InputStreamSource}.
58      *
59      * @param source the {@link InputStreamSource}
60      * @return a {@link String} containing the stream contents
61      * @throws IOException if failure occurred reading the stream
62      */
getStringFromSource(InputStreamSource source)63     public static String getStringFromSource(InputStreamSource source) throws IOException {
64         final InputStream stream = source.createInputStream();
65         final String contents;
66         try {
67             contents = getStringFromStream(stream);
68         } finally {
69             close(stream);
70         }
71         return contents;
72     }
73 
74     /**
75      * Count number of lines in an {@link InputStreamSource}
76      * @param source the {@link InputStreamSource}
77      * @return number of lines
78      * @throws IOException if failure occurred reading the stream
79      */
countLinesFromSource(InputStreamSource source)80     public static int countLinesFromSource(InputStreamSource source) throws IOException {
81         int lineCount = 0;
82         try (BufferedReader br =
83                 new BufferedReader(new InputStreamReader(source.createInputStream()))) {
84             while (br.readLine() != null) {
85                 lineCount++;
86             }
87         }
88         return lineCount;
89     }
90 
91     /**
92      * Retrieves a {@link ByteArrayList} from an {@link InputStreamSource}.
93      *
94      * @param source the {@link InputStreamSource}
95      * @return a {@link ByteArrayList} containing the stream contents
96      * @throws IOException if failure occurred reading the stream
97      */
getByteArrayListFromSource(InputStreamSource source)98     public static ByteArrayList getByteArrayListFromSource(InputStreamSource source)
99             throws IOException {
100         final InputStream stream = source.createInputStream();
101         final ByteArrayList contents;
102         try {
103             contents = getByteArrayListFromStream(stream);
104         } finally {
105             close(stream);
106         }
107         return contents;
108     }
109 
110     /**
111      * Retrieves a {@link String} from a character stream.
112      *
113      * @param stream the {@link InputStream}
114      * @return a {@link String} containing the stream contents
115      * @throws IOException if failure occurred reading the stream
116      */
getStringFromStream(InputStream stream)117     public static String getStringFromStream(InputStream stream) throws IOException {
118         return getStringFromStream(stream, 0);
119     }
120 
121     /**
122      * Retrieves a {@link String} from a character stream.
123      *
124      * @param stream the {@link InputStream}
125      * @param length the size of the content to read, set to 0 to read all contents
126      * @return a {@link String} containing the stream contents
127      * @throws IOException if failure occurred reading the stream
128      */
getStringFromStream(InputStream stream, long length)129     public static String getStringFromStream(InputStream stream, long length) throws IOException {
130         int irChar = -1;
131         StringBuilder builder = new StringBuilder();
132         try (Reader ir = new BufferedReader(new InputStreamReader(stream))) {
133             long count = 0;
134             while ((irChar = ir.read()) != -1) {
135                 builder.append((char) irChar);
136                 count++;
137                 if (length > 0 && count >= length) {
138                     break;
139                 }
140             }
141         }
142         return builder.toString();
143     }
144 
145     /**
146      * Retrieves a {@link ByteArrayList} from a byte stream.
147      *
148      * @param stream the {@link InputStream}
149      * @return a {@link ByteArrayList} containing the stream contents
150      * @throws IOException if failure occurred reading the stream
151      */
getByteArrayListFromStream(InputStream stream)152     public static ByteArrayList getByteArrayListFromStream(InputStream stream) throws IOException {
153         InputStream is = new BufferedInputStream(stream);
154         int inputByte = -1;
155         ByteArrayList list = new ByteArrayList();
156         while ((inputByte = is.read()) != -1) {
157             list.add((byte)inputByte);
158         }
159         list.trimToSize();
160         return list;
161     }
162 
163     /**
164      * Return a BuffferedReader to read the contents from the given InputstreamSource.
165      *
166      * @param stream the {@link InputStreamSource}
167      * @return a BuffferedReader
168      */
getBufferedReaderFromStreamSrc(InputStreamSource stream)169     public static BufferedReader getBufferedReaderFromStreamSrc(InputStreamSource stream) {
170         return new BufferedReader(new InputStreamReader(stream.createInputStream()));
171     }
172 
173     /**
174      * Copies contents of origStream to destStream.
175      * <p/>
176      * Recommended to provide a buffered stream for input and output
177      *
178      * @param inStream the {@link InputStream}
179      * @param outStream the {@link OutputStream}
180      * @throws IOException
181      */
copyStreams(InputStream inStream, OutputStream outStream)182     public static void copyStreams(InputStream inStream, OutputStream outStream)
183             throws IOException {
184         copyStreams(inStream, outStream, 0);
185     }
186 
187     /**
188      * Copies contents of origStream to destStream.
189      *
190      * <p>Recommended to provide a buffered stream for input and output
191      *
192      * @param inStream the {@link InputStream}
193      * @param outStream the {@link OutputStream}
194      * @param offset the offset of when to start copying the data.
195      * @throws IOException
196      */
copyStreams(InputStream inStream, OutputStream outStream, int offset)197     public static void copyStreams(InputStream inStream, OutputStream outStream, int offset)
198             throws IOException {
199         // Set size to a negative value to copy all content starting at the given offset.
200         copyStreams(inStream, outStream, offset, -1);
201     }
202 
203     /**
204      * Copies contents of origStream to destStream starting at a given offset with a specific size.
205      *
206      * <p>Recommended to provide a buffered stream for input and output
207      *
208      * @param inStream the {@link InputStream}
209      * @param outStream the {@link OutputStream}
210      * @param offset the offset of when to start copying the data.
211      * @param size the number of bytes to copy. A negative value means to copy all content.
212      * @throws IOException
213      */
copyStreams( InputStream inStream, OutputStream outStream, long offset, long size)214     public static void copyStreams(
215             InputStream inStream, OutputStream outStream, long offset, long size)
216             throws IOException {
217         Preconditions.checkArgument(offset >= 0, "offset must be greater or equal to zero.");
218         Preconditions.checkArgument(size != 0, "size cannot be zero.");
219         inStream.skip(offset);
220         byte[] buf = new byte[BUF_SIZE];
221         long totalRetrievedSize = 0;
222         try {
223             while (true) {
224                 int maxReadSize =
225                         size > 0
226                                 ? (int) Math.min(size - totalRetrievedSize, buf.length)
227                                 : buf.length;
228                 int retrievedSize = inStream.read(buf, 0, maxReadSize);
229                 if (retrievedSize == -1) {
230                     break;
231                 }
232                 outStream.write(buf, 0, retrievedSize);
233                 totalRetrievedSize += retrievedSize;
234                 if (size == totalRetrievedSize) {
235                     break;
236                 }
237             }
238         } catch (IOException e) {
239             throw FileUtil.convertToDiskSpaceIfNeeded(e);
240         }
241         if (size > 0 && size > totalRetrievedSize) {
242             throw new IOException(
243                     String.format(
244                             "Failed to read %d bytes starting at offset %d, only %d bytes "
245                                     + "retrieved.",
246                             size, offset, totalRetrievedSize));
247         }
248     }
249 
250     /**
251      * Copies contents of inStream to writer.
252      * <p/>
253      * Recommended to provide a buffered stream for input and output
254      *
255      * @param inStream the {@link InputStream}
256      * @param writer the {@link Writer} destination
257      * @throws IOException
258      */
copyStreamToWriter(InputStream inStream, Writer writer)259     public static void copyStreamToWriter(InputStream inStream, Writer writer) throws IOException {
260         byte[] buf = new byte[BUF_SIZE];
261         int size = -1;
262         while ((size = inStream.read(buf)) != -1) {
263             writer.write(new String(buf, 0, size));
264         }
265     }
266 
267     /**
268      * Copies contents of file to outStream. It is recommended to provide a buffered stream.
269      *
270      * @param file the {@link File}
271      * @param outStream the {@link OutputStream}
272      * @throws IOException
273      */
copyFileToStream(File file, OutputStream outStream)274     public static void copyFileToStream(File file, OutputStream outStream) throws IOException {
275         InputStream inStream = null;
276         try {
277             inStream = new FileInputStream(file);
278             inStream = new BufferedInputStream(inStream);
279             StreamUtil.copyStreams(inStream, outStream);
280         } finally {
281             StreamUtil.close(inStream);
282         }
283     }
284 
285     /**
286      * Gets the stack trace as a {@link String}.
287      *
288      * @param throwable the {@link Throwable} to convert.
289      * @return a {@link String} stack trace
290      */
getStackTrace(Throwable throwable)291     public static String getStackTrace(Throwable throwable) {
292         // dump the print stream results to the ByteArrayOutputStream, so contents can be evaluated
293         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
294         PrintStream bytePrintStream = new PrintStream(outputStream);
295         throwable.printStackTrace(bytePrintStream);
296         return outputStream.toString();
297     }
298 
299     /**
300      * @deprecated use {@link #close(Closeable)} instead.
301      */
302     @Deprecated
closeStream(OutputStream out)303     public static void closeStream(OutputStream out) {
304         close(out);
305     }
306 
307     /**
308      * @deprecated use {@link #close(Closeable)} instead.
309      */
310     @Deprecated
closeStream(InputStream in)311     public static void closeStream(InputStream in) {
312         close(in);
313     }
314 
315     /**
316      * Attempts to flush the given output stream, and then closes it.
317      *
318      * @param outStream the {@link OutputStream}. No action taken if outStream is null.
319      */
flushAndCloseStream(OutputStream outStream)320     public static void flushAndCloseStream(OutputStream outStream) {
321         if (outStream != null) {
322             try {
323                 outStream.flush();
324             } catch (IOException e) {
325                 // ignore
326             }
327             try {
328                 outStream.close();
329             } catch (IOException e) {
330                 // ignore
331             }
332         }
333     }
334 
335     /**
336      * Closes given zip output stream.
337      *
338      * @param outStream the {@link ZipOutputStream}. No action taken if outStream is null.
339      */
closeZipStream(ZipOutputStream outStream)340     public static void closeZipStream(ZipOutputStream outStream) {
341         if (outStream != null) {
342             try {
343                 outStream.closeEntry();
344                 outStream.close();
345             } catch (IOException e) {
346                 // ignore
347             }
348         }
349     }
350 
351     /**
352      * Closes given gzip output stream.
353      *
354      * @param outStream the {@link ZipOutputStream}. No action taken if outStream is null.
355      */
closeGZipStream(GZIPOutputStream outStream)356     public static void closeGZipStream(GZIPOutputStream outStream) {
357         if (outStream != null) {
358             try {
359                 outStream.finish();
360                 outStream.close();
361             } catch (IOException e) {
362                 // ignore
363             }
364         }
365     }
366 
367     /**
368      * Closes the given {@link Closeable}.
369      *
370      * @param closeable the {@link Closeable}. No action taken if <code>null</code>.
371      */
close(Closeable closeable)372     public static void close(Closeable closeable) {
373         if (closeable != null) {
374             try {
375                 closeable.close();
376             } catch (IOException e) {
377                 // ignore
378             }
379         }
380     }
381 
382     /**
383      * Cancels the given {@link InputStreamSource} if non-null.
384      */
cancel(InputStreamSource outputSource)385     public static void cancel(InputStreamSource outputSource) {
386         if (outputSource != null) {
387             outputSource.close();
388         }
389     }
390 
391     /**
392      * Create a {@link OutputStream} that discards all writes.
393      */
nullOutputStream()394     public static OutputStream nullOutputStream() {
395         return ByteStreams.nullOutputStream();
396     }
397 
398     /**
399      * Helper method to calculate CRC-32 for an {@link InputStream}. The stream will be consumed and
400      * closed. It is recommended to provide a buffered stream.
401      *
402      * @param inStream the {@link InputStream}
403      * @return CRC-32 of the stream
404      * @throws IOException
405      */
calculateCrc32(InputStream inStream)406     public static long calculateCrc32(InputStream inStream) throws IOException {
407         CRC32 crc32 = new CRC32();
408         byte[] buf = new byte[BUF_SIZE];
409         int size = -1;
410         try {
411             while ((size = inStream.read(buf)) >= 0) {
412                 crc32.update(buf, 0, size);
413             }
414         } finally {
415             inStream.close();
416         }
417         return crc32.getValue();
418     }
419 
420     /**
421      * Helper method to calculate md5 for a inputStream. The inputStream will be consumed and
422      * closed.
423      *
424      * @param inputSource used to create inputStream
425      * @return md5 of the stream
426      * @throws IOException
427      */
calculateMd5(InputStream inputSource)428     public static String calculateMd5(InputStream inputSource) throws IOException {
429         return bytesToHexString(calculateMd5Digest(inputSource));
430     }
431 
432     /**
433      * Helper method to calculate base64 md5 for a inputStream. The inputStream will be consumed and
434      * closed.
435      *
436      * @param inputSource used to create inputStream
437      * @return base64 md5 of the stream
438      * @throws IOException
439      */
calculateBase64Md5(InputStream inputSource)440     public static String calculateBase64Md5(InputStream inputSource) throws IOException {
441         return Base64.getEncoder().encodeToString(calculateMd5Digest(inputSource));
442     }
443 
calculateMd5Digest(InputStream inputSource)444     private static byte[] calculateMd5Digest(InputStream inputSource) throws IOException {
445         MessageDigest md = null;
446         try {
447             md = MessageDigest.getInstance("md5");
448         } catch (NoSuchAlgorithmException e) {
449             // This should not happen
450             throw new RuntimeException(e);
451         }
452         InputStream input = new BufferedInputStream(new DigestInputStream(inputSource, md));
453         byte[] buf = new byte[BUF_SIZE];
454         while (input.read(buf) != -1) {
455             // Read through the stream to update digest.
456         }
457         input.close();
458         return md.digest();
459     }
460 
461     private static final char[] HEX_CHARS = {
462         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
463     };
464 
465     /**
466      * Converts a byte array into a String of hexadecimal characters.
467      *
468      * @param bytes an array of bytes
469      * @return hex string representation of bytes array
470      */
bytesToHexString(byte[] bytes)471     private static String bytesToHexString(byte[] bytes) {
472         Objects.requireNonNull(bytes);
473         StringBuilder sb = new StringBuilder(2 * bytes.length);
474         for (int i = 0; i < bytes.length; i++) {
475             int b = 0x0f & (bytes[i] >> 4);
476             sb.append(HEX_CHARS[b]);
477             b = 0x0f & bytes[i];
478             sb.append(HEX_CHARS[b]);
479         }
480         return sb.toString();
481     }
482 }
483