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
17package libcore.java.util.zip;
18
19import java.io.ByteArrayInputStream;
20import java.io.ByteArrayOutputStream;
21import java.io.EOFException;
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileOutputStream;
25import java.io.IOException;
26import java.io.InputStream;
27import java.nio.charset.StandardCharsets;
28import java.util.Arrays;
29import java.util.Random;
30import java.util.zip.GZIPInputStream;
31import java.util.zip.GZIPOutputStream;
32import junit.framework.TestCase;
33import libcore.io.IoUtils;
34import libcore.io.Streams;
35
36public final class GZIPInputStreamTest extends TestCase {
37
38    private static final byte[] HELLO_WORLD_GZIPPED = new byte[] {
39        31, -117, 8, 0, 0, 0, 0, 0, 0, 0,  // 10 byte header
40        -13, 72, -51, -55, -55, 87, 8, -49, 47, -54, 73, 1, 0, 86, -79, 23, 74, 11, 0, 0, 0  // data
41    };
42
43    /**
44     * This is the same as the above, except that the 4th header byte is 2 (FHCRC flag)
45     * and the 2 bytes after the header make up the CRC.
46     *
47     * Constructed manually because none of the commonly used tools appear to emit header CRCs.
48     */
49    private static final byte[] HELLO_WORLD_GZIPPED_WITH_HEADER_CRC = new byte[] {
50        31, -117, 8, 2, 0, 0, 0, 0, 0, 0,  // 10 byte header
51        29, 38, // 2 byte CRC.
52        -13, 72, -51, -55, -55, 87, 8, -49, 47, -54, 73, 1, 0, 86, -79, 23, 74, 11, 0, 0, 0  // data
53    };
54
55    /*(
56     * This is the same as {@code HELLO_WORLD_GZIPPED} except that the 4th header byte is 4
57     * (FEXTRA flag) and that the 8 bytes after the header make up the extra.
58     *
59     * Constructed manually because none of the commonly used tools appear to emit header CRCs.
60     */
61    private static final byte[] HELLO_WORLD_GZIPPED_WITH_EXTRA = new byte[] {
62        31, -117, 8, 4, 0, 0, 0, 0, 0, 0,  // 10 byte header
63        6, 0, 4, 2, 4, 2, 4, 2,  // 2 byte extra length + 6 byte extra.
64        -13, 72, -51, -55, -55, 87, 8, -49, 47, -54, 73, 1, 0, 86, -79, 23, 74, 11, 0, 0, 0  // data
65    };
66
67    public void testShortMessage() throws IOException {
68        assertEquals("Hello World", new String(gunzip(HELLO_WORLD_GZIPPED), "UTF-8"));
69    }
70
71    public void testShortMessageWithCrc() throws IOException {
72        assertEquals("Hello World", new String(gunzip(HELLO_WORLD_GZIPPED_WITH_HEADER_CRC), "UTF-8"));
73    }
74
75    public void testShortMessageWithHeaderExtra() throws IOException {
76        assertEquals("Hello World", new String(gunzip(HELLO_WORLD_GZIPPED_WITH_EXTRA), "UTF-8"));
77    }
78
79    public void testLongMessage() throws IOException {
80        byte[] data = new byte[1024 * 1024];
81        new Random().nextBytes(data);
82        assertTrue(Arrays.equals(data, gunzip(GZIPOutputStreamTest.gzip(data))));
83    }
84
85    /** http://b/3042574 GzipInputStream.skip() causing CRC failures */
86    public void testSkip() throws IOException {
87        byte[] data = new byte[1024 * 1024];
88        byte[] gzipped = GZIPOutputStreamTest.gzip(data);
89        GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(gzipped));
90        long totalSkipped = 0;
91
92        long count;
93        do {
94            count = in.skip(Long.MAX_VALUE);
95            totalSkipped += count;
96        } while (count > 0);
97
98        assertEquals(data.length, totalSkipped);
99        in.close();
100    }
101
102    // https://code.google.com/p/android/issues/detail?id=63873
103    public void testMultipleMembers() throws Exception {
104        final int length = HELLO_WORLD_GZIPPED.length;
105        byte[] data = new byte[length * 2];
106        System.arraycopy(HELLO_WORLD_GZIPPED, 0, data, 0, length);
107        System.arraycopy(HELLO_WORLD_GZIPPED, 0, data, length, length);
108
109        assertEquals("Hello WorldHello World", new String(gunzip(data), "UTF-8"));
110    }
111
112    // https://code.google.com/p/android/issues/detail?id=63873
113    public void testTrailingNonGzipData() throws Exception {
114        final int length = HELLO_WORLD_GZIPPED.length;
115        // 50 bytes of 0s at the end of the first message.
116        byte[] data = new byte[length  + 50];
117        System.arraycopy(HELLO_WORLD_GZIPPED, 0, data, 0, length);
118        assertEquals("Hello World", new String(gunzip(data), "UTF-8"));
119    }
120
121    // https://code.google.com/p/android/issues/detail?id=63873
122    //
123    // Differences from the RI: Tests show the RI ignores *some* types of partial
124    // data but not others and this test case fails as a result. Our implementation
125    // will throw if it sees the gzip magic sequence at the end of a member
126    // but malformed / invalid data after.
127    public void testTrailingHeaderAndPartialMember() throws Exception {
128        final int length = HELLO_WORLD_GZIPPED.length;
129        // Copy just the header from HELLO_WORLD_GZIPPED so that our input
130        // stream becomes one complete member + a header member.
131        byte[] data = new byte[length  + 10];
132        System.arraycopy(HELLO_WORLD_GZIPPED, 0, data, 0, length);
133        System.arraycopy(HELLO_WORLD_GZIPPED, 0, data, length, 10);
134
135        // The trailing data is ignored since the amount of data available is
136        // less than the size of a header + trailer (which is the absolute minimum required).
137        byte[] unzipped = gunzip(data);
138        assertEquals("Hello World", new String(unzipped, StandardCharsets.UTF_8));
139
140        // Must fail here because the data contains a full header and partial data.
141        data = new byte[length*2 - 4];
142        System.arraycopy(HELLO_WORLD_GZIPPED, 0, data, 0, length);
143        System.arraycopy(HELLO_WORLD_GZIPPED, 0, data, length, length - 4);
144
145        try {
146            gunzip(data);
147            fail();
148        } catch (EOFException expected) {
149        }
150    }
151
152    // https://code.google.com/p/android/issues/detail?id=66409
153    public void testMultipleMembersWithCustomBufferSize() throws Exception {
154        final int[] memberSizes = new int[] { 1000, 2000 };
155
156        // We don't care what the exact contents of this file is, as long
157        // as the file has multiple members, and that the (compressed) size of
158        // the second member is larger than the size of the input buffer.
159        //
160        // There's no way to achieve this for a GZIPOutputStream so we generate
161        // pseudo-random sequence of bytes and assert that they don't compress
162        // well.
163        final Random r = new Random(10);
164        byte[] bytes = new byte[3000];
165        r.nextBytes(bytes);
166
167        File f = File.createTempFile("GZIPInputStreamTest", ".gzip");
168        int offset = 0;
169        for (int size : memberSizes) {
170            GZIPOutputStream gzos = null;
171            try {
172                FileOutputStream fos = new FileOutputStream(f, true /* append */);
173                gzos = new GZIPOutputStream(fos, size + 1);
174                gzos.write(bytes, offset, size);
175                offset += size;
176                gzos.finish();
177            } finally {
178                IoUtils.closeQuietly(gzos);
179            }
180        }
181
182        assertTrue(f.length() > 2048);
183
184        FileInputStream fis = new FileInputStream(f);
185        GZIPInputStream gzip = null;
186        try {
187            gzip = new GZIPInputStream(fis, memberSizes[0]);
188            byte[] unzipped = Streams.readFully(gzip);
189            assertTrue(Arrays.equals(bytes, unzipped));
190        } finally {
191            IoUtils.closeQuietly(gzip);
192        }
193    }
194
195    public static byte[] gunzip(byte[] bytes) throws IOException {
196        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
197        InputStream in = new GZIPInputStream(bis);
198        ByteArrayOutputStream out = new ByteArrayOutputStream();
199        byte[] buffer = new byte[1024];
200        int count;
201        while ((count = in.read(buffer)) != -1) {
202            out.write(buffer, 0, count);
203        }
204
205        byte[] outArray = out.toByteArray();
206        in.close();
207
208        return outArray;
209    }
210}
211