// Copyright 2015 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.android.ziputils;

import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENHOW;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENLEN;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENOFF;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENSIZ;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDOFF;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIG;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIZ;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSUB;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDTOT;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;

import com.google.devtools.build.android.ziputils.ZipIn.ZipEntry;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipInputStream;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Unit tests for {@link ZipIn}.
 */
@RunWith(JUnit4.class)
public class ZipInTest {

  private static final int ENTRY_COUNT = 1000;
  private FakeFileSystem fileSystem;

  @Before
  public void setUp() throws Exception {
    fileSystem = new FakeFileSystem();
  }

  /**
   * Test of endOfCentralDirectory method, of class ZipIn.
   */
  @Test
  public void testEndOfCentralDirectory() throws Exception {

    String filename = "test.zip";
    byte[] bytes;
    ByteBuffer buffer;
    String comment;
    int commentLen;
    int offset;
    ZipIn zipIn;
    EndOfCentralDirectory result;
    String subcase;

    // Find it, even if it's the only useful thing in the file.
    subcase = " EOCD found it, ";
    bytes = new byte[] {
      0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };
    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    result = zipIn.endOfCentralDirectory();
    assertNotNull(subcase + "found", result);

    subcase = " EOCD not there at all, ";
    bytes = new byte[]{
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };
    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    try {
      zipIn.endOfCentralDirectory();
      fail(subcase + "expected IllegalStateException");
    } catch (Exception ex) {
      assertSame(subcase + "caught exception", IllegalStateException.class, ex.getClass());
    }

    // If we can't read it, it's not there
    subcase = " EOCD too late to read, ";
    bytes = new byte[] {
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };
    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    try {
      zipIn.endOfCentralDirectory();
      fail(subcase + "expected IndexOutOfBoundsException");
    } catch (Exception ex) {
      assertSame(subcase + "caught exception", IndexOutOfBoundsException.class, ex.getClass());
    }

    // Current implementation doesn't know to scan past a bad EOCD record.
    // I'm not sure if it should.
    subcase = " EOCD good hidden by bad, ";
    bytes = new byte[] {
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };
    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    try {
      zipIn.endOfCentralDirectory();
      fail(subcase + "expected IndexOutOfBoundsException");
    } catch (Exception ex) {
      assertSame(subcase + "caught exception", IndexOutOfBoundsException.class, ex.getClass());
    }

    // Minimal format checking here, assuming the EndOfDirectoryTest class
    // test for that.

    subcase = " EOCD truncated comment, ";
    bytes = new byte[100];
    buffer = ByteBuffer.wrap(bytes);
    comment = "optional file comment";
    commentLen = comment.getBytes(UTF_8).length;
    offset = bytes.length - ZipInputStream.ENDHDR - commentLen;
    buffer.position(offset);
    EndOfCentralDirectory.view(buffer, comment);
    byte[] truncated = Arrays.copyOf(bytes, bytes.length - 5);
    fileSystem.addFile(filename, truncated);
    zipIn = newZipIn(filename);
    try { // not sure this is the exception we want!
      zipIn.endOfCentralDirectory();
      fail(subcase + "expected IllegalArgumentException");
    } catch (Exception ex) {
      assertSame(subcase + "caught exception", IllegalArgumentException.class, ex.getClass());
    }

    subcase = " EOCD no comment, ";
    bytes = new byte[100];
    buffer = ByteBuffer.wrap(bytes);
    comment = null;
    commentLen = 0;
    offset = bytes.length - ZipInputStream.ENDHDR - commentLen;
    buffer.position(offset);
    EndOfCentralDirectory.view(buffer, comment);
    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    result = zipIn.endOfCentralDirectory();
    assertNotNull(subcase + "found", result);
    assertEquals(subcase + "comment", "", result.getComment());
    assertEquals(subcase + "marker", ZipInputStream.ENDSIG, (int) result.get(ENDSIG));

    subcase = " EOCD comment, ";
    bytes = new byte[100];
    buffer = ByteBuffer.wrap(bytes);
    comment = "optional file comment";
    commentLen = comment.getBytes(UTF_8).length;
    offset = bytes.length - ZipInputStream.ENDHDR - commentLen;
    buffer.position(offset);
    EndOfCentralDirectory.view(buffer, comment);
    assertEquals(subcase + "setup", comment,
        new String(bytes, bytes.length - commentLen, commentLen, UTF_8));
    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    result = zipIn.endOfCentralDirectory();
    assertNotNull(subcase + "found", result);
    assertEquals(subcase + "comment", comment, result.getComment());
    assertEquals(subcase + "marker", ZipInputStream.ENDSIG, (int) result.get(ENDSIG));

    subcase = " EOCD extra data, ";
    bytes = new byte[100];
    buffer = ByteBuffer.wrap(bytes);
    comment = null;
    commentLen = 0;
    offset = bytes.length - ZipInputStream.ENDHDR - commentLen - 10;
    buffer.position(offset);
    EndOfCentralDirectory.view(buffer, comment);
    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    result = zipIn.endOfCentralDirectory();
    assertNotNull(subcase + "found", result);
    assertEquals(subcase + "comment", "", result.getComment());
    assertEquals(subcase + "marker", ZipInputStream.ENDSIG, (int) result.get(ENDSIG));
  }

  /**
   * Test of centralDirectory method, of class ZipIn.
   */
  @Test
  public void testCentralDirectory() throws Exception {
    String filename = "test.zip";
    ByteBuffer buffer;
    int offset;
    ZipIn zipIn;
    String subcase;
    subcase = " EOCD extra data, ";
    String commonName = "thisIsNotNormal.txt";
    int filenameLen = commonName.getBytes(UTF_8).length;
    int count = ENTRY_COUNT;
    int dirEntry = ZipInputStream.CENHDR;
    int before = count;
    int between = 0; // implementation doesn't tolerate data between dir entries, does the spec?
    int after = 20;
    int eocd = ZipInputStream.ENDHDR;
    int total = before + (count * (dirEntry + filenameLen)) + ((count - 1) * between)
        + after + eocd;
    byte[] bytes = new byte[total];
    offset = before;
    for (int i = 0; i < count; i++) {
      if (i > 0) {
        offset += between;
      }
      buffer = ByteBuffer.wrap(bytes, offset, bytes.length - offset);
      DirectoryEntry.view(buffer, commonName, null, null)
          .set(CENHOW, (short) 8)
          .set(CENSIZ, before)
          .set(CENLEN, 2 * before)
          .set(CENOFF, i); // Not valid of course, but we're only testing central dir parsing.
          // and there are currently no checks in the parser to see if offset makes sense.
      offset += dirEntry + filenameLen;
    }
    offset += after;
    buffer = ByteBuffer.wrap(bytes, offset, bytes.length - offset);
    EndOfCentralDirectory.view(buffer, null)
        .set(ENDOFF, before)
        .set(ENDSIZ, offset - before - after)
        .set(ENDTOT, (short) count)
        .set(ENDSUB, (short) count);

    fileSystem.addFile(filename, bytes);
    zipIn = newZipIn(filename);
    CentralDirectory result = zipIn.centralDirectory();
    assertNotNull(subcase + "found", result);
    List<DirectoryEntry> list = result.list();
    assertEquals(subcase + "size", count, list.size());
    for (int i = 0; i < list.size(); i++) {
      assertEquals(subcase + "offset check[" + i + "]", i, list.get(i).get(CENOFF));
    }
  }

  /**
   * Test of scanEntries method, of class ZipIn.
   */
  @Test
  public void testScanEntries() throws Exception {
    int count = ENTRY_COUNT * 100;
    String filename = "test.jar";

    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);

    final ZipIn zipIn = newZipIn(filename);
    zipIn.scanEntries(new EntryHandler() {
      int count = 0;
      @Override
      public void handle(ZipIn in, LocalFileHeader header, DirectoryEntry dirEntry,
          ByteBuffer data) throws IOException {
        assertSame(zipIn, in);
        String filename = "pkg/f" + count + ".class";
        assertEquals(filename, header.getFilename());
        assertEquals(filename, dirEntry.getFilename());
        count++;
      }
    });
  }

  /**
   * Test of nextHeaderFrom method, of class ZipIn.
   */
  @Test
  public void testNextHeaderFrom_long() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.endOfCentralDirectory();
    count = 0;
    int offset = 0;
    LocalFileHeader header;
    do {
      header = zipIn.nextHeaderFrom(offset);
      String name = "pkg/f" + count + ".class";
      if (header != null) {
        assertEquals(name, header.getFilename());
        count++;
        offset = (int) header.fileOffset() + 4;
      }
    } while(header != null);
    assertEquals(ENTRY_COUNT, count);
  }

  /**
   * Test of nextHeaderFrom method, of class ZipIn.
   */
  @Test
  public void testNextHeaderFrom_DirectoryEntry() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.centralDirectory();
    List<DirectoryEntry> list = zipIn.centralDirectory().list();
    count = 0;
    String name;
    LocalFileHeader header = zipIn.nextHeaderFrom(null);
    for (DirectoryEntry dirEntry : list) {
      name = "pkg/f" + count + ".class";
      assertEquals(name, dirEntry.getFilename());
      assertEquals(name, header.getFilename());
      header = zipIn.nextHeaderFrom(dirEntry);
      count++;
    }
    assertNull(header);
  }

  /**
   * Test of localHeaderFor method, of class ZipIn.
   */
  @Test
  public void testLocalHeaderFor() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.centralDirectory();
    List<DirectoryEntry> list = zipIn.centralDirectory().list();
    count = 0;
    String name;
    LocalFileHeader header;
    for (DirectoryEntry dirEntry : list) {
      name = "pkg/f" + count + ".class";
      header = zipIn.localHeaderFor(dirEntry);
      assertEquals(name, dirEntry.getFilename());
      assertEquals(name, header.getFilename());
      count++;
    }
  }

  /**
   * Test of localHeaderAt method, of class ZipIn.
   */
  @Test
  public void testLocalHeaderAt() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.centralDirectory();
    List<DirectoryEntry> list = zipIn.centralDirectory().list();
    count = 0;
    String name;
    LocalFileHeader header;
    for (DirectoryEntry dirEntry : list) {
      name = "pkg/f" + count + ".class";
      header = zipIn.localHeaderAt(dirEntry.get(CENOFF));
      assertEquals(name, dirEntry.getFilename());
      assertEquals(name, header.getFilename());
      count++;
    }
  }

  /**
   * Test of nextFrom method, of class ZipIn.
   */
  @Test
  public void testNextFrom_long() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.centralDirectory();
    count = 0;
    int offset = 0;
    ZipEntry zipEntry;
    do {
      zipEntry = zipIn.nextFrom(offset);
      String name = "pkg/f" + count + ".class";
      if (zipEntry.getCode() != ZipEntry.Status.ENTRY_NOT_FOUND) {
        assertNotNull(zipEntry.getHeader());
        assertNotNull(zipEntry.getDirEntry());
        assertEquals(name, zipEntry.getHeader().getFilename());
        assertEquals(name, zipEntry.getDirEntry().getFilename());
        count++;
        offset = (int) zipEntry.getHeader().fileOffset() + 4;
      }
    } while(zipEntry.getCode() != ZipEntry.Status.ENTRY_NOT_FOUND);
    assertEquals(ENTRY_COUNT, count);
  }

  /**
   * Test of nextFrom method, of class ZipIn.
   */
  @Test
  public void testNextFrom_DirectoryEntry() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.centralDirectory();
    List<DirectoryEntry> list = zipIn.centralDirectory().list();
    count = 0;
    String name;
    ZipEntry zipEntry = zipIn.nextFrom(null);
    for (DirectoryEntry dirEntry : list) {
      if (zipEntry.getCode() == ZipEntry.Status.ENTRY_NOT_FOUND) {
        break;
      }
      name = "pkg/f" + count + ".class";
      assertNotNull(zipEntry.getHeader());
      assertNotNull(zipEntry.getDirEntry());
      assertEquals(name, zipEntry.getHeader().getFilename());
      assertEquals(name, zipEntry.getDirEntry().getFilename());
      zipEntry = zipIn.nextFrom(dirEntry);
      count++;
    }
    assertEquals(ENTRY_COUNT, count);
  }

  /**
   * Test of entryAt method, of class ZipIn.
   */
  @Test
  public void testEntryAt() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.centralDirectory();
    List<DirectoryEntry> list = zipIn.centralDirectory().list();
    count = 0;
    String name;
    ZipEntry zipEntry;
    for (DirectoryEntry dirEntry : list) {
      zipEntry = zipIn.entryAt(dirEntry.get(CENOFF));
      name = "pkg/f" + count + ".class";
      assertNotNull(zipEntry.getHeader());
      assertNotNull(zipEntry.getDirEntry());
      assertEquals(name, zipEntry.getHeader().getFilename());
      assertEquals(name, zipEntry.getDirEntry().getFilename());
      count++;
    }
    assertEquals(ENTRY_COUNT, count);
  }

  /**
   * Test of entryWith method, of class ZipIn.
   */
  @Test
  public void testEntryWith() throws Exception {
    int count = ENTRY_COUNT;
    String filename = "test.jar";
    ZipFileBuilder builder = new ZipFileBuilder();
    for (int i = 0; i < count; i++) {
      builder.add("pkg/f" + i + ".class", "All day long");
    }
    builder.create(filename);
    final ZipIn zipIn = newZipIn(filename);
    zipIn.centralDirectory();
    count = 0;
    int offset = 0;
    LocalFileHeader header;
    do {
      header = zipIn.nextHeaderFrom(offset);
      String name = "pkg/f" + count + ".class";
      if (header != null) {
        ZipEntry zipEntry = zipIn.entryWith(header);
        assertNotNull(zipEntry.getDirEntry());
        assertSame(header, zipEntry.getHeader());
        assertEquals(name, zipEntry.getHeader().getFilename());
        assertEquals(name, zipEntry.getDirEntry().getFilename());
        assertEquals(name, header.getFilename());
        count++;
        offset = (int) header.fileOffset() + 4;
      }
    } while(header != null);
    assertEquals(ENTRY_COUNT, count);
  }

  private ZipIn newZipIn(String filename) throws IOException {
    return new ZipIn(fileSystem.getInputChannel(filename), filename);
  }
}
