// Copyright 2014 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.xcode.zip;

import com.google.common.collect.ImmutableMap;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

/**
 * Utility code for reading information from zip files.
 */
public final class ZipFiles {
  /** Read a little-endian integer comprised of {@code count} bytes from the input channel. */
  private static int readBytes(int count, ReadableByteChannel input) throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(count);
    if (input.read(buffer) != count) {
      throw new IOException("could not read expected number of bytes: " + count);
    }
    int result = 0;
    for (int i = count - 1; i >= 0; i--) {
      result <<= 8;
      result |= buffer.get(i) & 0xff;
    }
    return result;
  }

  /**
   * Returns the external file attributes of each entry as a mapping from the entry name to the
   * 32-bit value. As long as the attributes are generated by a Unix host, this includes the POSIX
   * file permissions in the upper two bytes. Entries not generated by a Unix host are not included
   * in the result.
   */
  public static Map<String, Integer> unixExternalFileAttributes(Path zipFile) throws IOException {
    // Field descriptions in comments were taken from this document:
    // http://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
    ImmutableMap.Builder<String, Integer> attributes = new ImmutableMap.Builder<>();
    try (SeekableByteChannel input = Files.newByteChannel(zipFile)) {
      // The data we care about is toward the end of the file, after the compressed data for each
      // file. We begin by looking for the start of the end of central directory record, which is
      // marked by the signature 0x06054b50
      //
      // This contains the centralDirectoryStartOffset value, which tells us where to seek to find
      // the first central directory entry. Each such entry is marked by the signature 0x02014b50
      // and appear in sequence, one entry for each file in the .zip.
      //
      // The central directory entry contains many values, including the file name, the external
      // file attributes, and the version made by value. If the version made by indicates a Unix
      // host (0x03??), we include the external file attributes in the returned map.

      long offset = input.size() - 4;
      while (offset >= 0) {
        input.position(offset);
        int signature = readBytes(4, input);
        if (signature == 0x06054b50) {
          break;
        } else if (signature == 0x06064b50) {
          throw new IOException("Zip64 format not supported: " + zipFile);
        }
        offset--;
      }
      if (offset < 0) {
        throw new IOException();
      }

      // Read end of central directory structure
      input.position(input.position()
          + 2 // number of this disk
          + 2 // number of the disk with the start of the central directory
      );
      int entryCount = readBytes(2, input);
      input.position(input.position()
          + 2 // total number of entries in the central directory
      );
      input.position(input.position()
          + 4 // size of the central directory
      );
      int centralDirectoryStartOffset = readBytes(4, input);
      if (0xffffffff == centralDirectoryStartOffset) {
        throw new IOException("Zip64 format not supported.");
      }

      input.position(centralDirectoryStartOffset);
      int entriesFound = 0;

      // Read each central directory entry
      while ((entriesFound < entryCount) && (readBytes(4, input) == 0x02014b50)) {
        int versionMadeBy = readBytes(2, input);
        input.position(input.position()
            + 2 // version needed to extract
            + 2 // general purpose bit flag
            + 2 // compression method
            + 2 // last mod file time
            + 2 // last mod file date
            + 4 // crc-32
            + 4 // compressed size
            + 4 // uncompressed size
        );
        int filenameLength = readBytes(2, input);
        int extraFieldLength = readBytes(2, input);
        int fileCommentLength = readBytes(2, input);
        input.position(input.position()
            + 2 // disk number start
            + 2 // internal file attributes
        );
        int externalFileAttributes = readBytes(4, input);
        input.position(input.position()
            + 4 // relative offset of local header
        );
        ByteBuffer filenameBuffer = ByteBuffer.allocate(filenameLength);
        if (filenameLength != input.read(filenameBuffer)) {
          throw new IOException(
              String.format(
                  "Could not read file name (length %d) in central directory record",
                  filenameLength));
        }
        input.position(input.position() + extraFieldLength + fileCommentLength);
        entriesFound++;
        if ((versionMadeBy >> 8) == 3) {
          // Zip made by a Unix host - the external file attributes are POSIX permissions.
          String filename = new String(filenameBuffer.array(), StandardCharsets.UTF_8);
          attributes.put(filename, externalFileAttributes);
        }
      }
      if (entriesFound != entryCount) {
        System.err.printf(
            "WARNING: Expected %d entries in central directory record in '%s', but found %d\n",
            entryCount, zipFile, entriesFound);
      }
    }
    return attributes.build();
  }

  private ZipFiles() {}
}
