package cn.hutool.core.util;

import cn.hutool.core.collection.EnumerationIter;
import cn.hutool.core.compress.Deflate;
import cn.hutool.core.compress.Gzip;
import cn.hutool.core.compress.ZipCopyVisitor;
import cn.hutool.core.compress.ZipReader;
import cn.hutool.core.compress.ZipWriter;
import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.LimitedInputStream;
import cn.hutool.core.io.file.FileSystemUtil;
import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.io.resource.Resource;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.CopyOption;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.function.Consumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 压缩工具类
 *
 * @author Looly
 * @see cn.hutool.core.compress.ZipWriter
 */
public class ZipUtil {

	private static final int DEFAULT_BYTE_ARRAY_LENGTH = 32;

	/**
	 * 默认编码，使用平台相关编码
	 */
	private static final Charset DEFAULT_CHARSET = CharsetUtil.defaultCharset();

	/**
	 * 将Zip文件转换为{@link ZipFile}
	 *
	 * @param file    zip文件
	 * @param charset 解析zip文件的编码，null表示{@link CharsetUtil#CHARSET_UTF_8}
	 * @return {@link ZipFile}
	 */
	public static ZipFile toZipFile(File file, Charset charset) {
		try {
			return new ZipFile(file, ObjectUtil.defaultIfNull(charset, CharsetUtil.CHARSET_UTF_8));
		} catch (IOException e) {
			throw new IORuntimeException(e);
		}
	}

	/**
	 * 获取指定{@link ZipEntry}的流，用于读取这个entry的内容<br>
	 * 此处使用{@link LimitedInputStream} 限制最大写出大小，避免ZIP bomb漏洞
	 *
	 * @param zipFile  {@link ZipFile}
	 * @param zipEntry {@link ZipEntry}
	 * @return 流
	 * @since 5.5.2
	 */
	public static InputStream getStream(ZipFile zipFile, ZipEntry zipEntry) {
		try {
			return new LimitedInputStream(zipFile.getInputStream(zipEntry), zipEntry.getSize());
		} catch (IOException e) {
			throw new IORuntimeException(e);
		}
	}

	/**
	 * 获得 {@link ZipOutputStream}
	 *
	 * @param out     压缩文件流
	 * @param charset 编码
	 * @return {@link ZipOutputStream}
	 * @since 5.8.0
	 */
	public static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) {
		if (out instanceof ZipOutputStream) {
			return (ZipOutputStream) out;
		}
		return new ZipOutputStream(out, charset);
	}

	/**
	 * 在zip文件中添加新文件或目录<br>
	 * 新文件添加在zip根目录，文件夹包括其本身和内容<br>
	 * 如果待添加文件夹是系统根路径（如/或c:/），则只复制文件夹下的内容
	 *
	 * @param zipPath        zip文件的Path
	 * @param appendFilePath 待添加文件Path(可以是文件夹)
	 * @param options        拷贝选项，可选是否覆盖等
	 * @throws IORuntimeException IO异常
	 * @since 5.7.15
	 */
	public static void append(Path zipPath, Path appendFilePath, CopyOption... options) throws IORuntimeException {
		try (FileSystem zipFileSystem = FileSystemUtil.createZip(zipPath.toString())) {
			if (Files.isDirectory(appendFilePath)) {
				Path source = appendFilePath.getParent();
				if (null == source) {
					// 如果用户提供的是根路径，则不复制目录，直接复制目录下的内容
					source = appendFilePath;
				}
				Files.walkFileTree(appendFilePath, new ZipCopyVisitor(source, zipFileSystem, options));
			} else {
				Files.copy(appendFilePath, zipFileSystem.getPath(PathUtil.getName(appendFilePath)), options);
			}
		} catch (FileAlreadyExistsException ignored) {
			// 不覆盖情况下，文件已存在, 跳过
		} catch (IOException e) {
			throw new IORuntimeException(e);
		}
	}

	/**
	 * 打包到当前目录，使用默认编码UTF-8
	 *
	 * @param srcPath 源文件路径
	 * @return 打包好的压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(String srcPath) throws UtilException {
		return zip(srcPath, DEFAULT_CHARSET);
	}

	/**
	 * 打包到当前目录
	 *
	 * @param srcPath 源文件路径
	 * @param charset 编码
	 * @return 打包好的压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(String srcPath, Charset charset) throws UtilException {
		return zip(FileUtil.file(srcPath), charset);
	}

	/**
	 * 打包到当前目录，使用默认编码UTF-8
	 *
	 * @param srcFile 源文件或目录
	 * @return 打包好的压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(File srcFile) throws UtilException {
		return zip(srcFile, DEFAULT_CHARSET);
	}

	/**
	 * 打包到当前目录
	 *
	 * @param srcFile 源文件或目录
	 * @param charset 编码
	 * @return 打包好的压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(File srcFile, Charset charset) throws UtilException {
		final File zipFile = FileUtil.file(srcFile.getParentFile(), FileUtil.mainName(srcFile) + ".zip");
		zip(zipFile, charset, false, srcFile);
		return zipFile;
	}

	/**
	 * 对文件或文件目录进行压缩<br>
	 * 不包含被打包目录
	 *
	 * @param srcPath 要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
	 * @param zipPath 压缩文件保存的路径，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @return 压缩好的Zip文件
	 * @throws UtilException IO异常
	 */
	public static File zip(String srcPath, String zipPath) throws UtilException {
		return zip(srcPath, zipPath, false);
	}

	/**
	 * 对文件或文件目录进行压缩<br>
	 *
	 * @param srcPath    要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
	 * @param zipPath    压缩文件保存的路径，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param withSrcDir 是否包含被打包目录
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(String srcPath, String zipPath, boolean withSrcDir) throws UtilException {
		return zip(srcPath, zipPath, DEFAULT_CHARSET, withSrcDir);
	}

	/**
	 * 对文件或文件目录进行压缩<br>
	 *
	 * @param srcPath    要压缩的源文件路径。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
	 * @param zipPath    压缩文件保存的路径，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param charset    编码
	 * @param withSrcDir 是否包含被打包目录
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(String srcPath, String zipPath, Charset charset, boolean withSrcDir) throws UtilException {
		final File srcFile = FileUtil.file(srcPath);
		final File zipFile = FileUtil.file(zipPath);
		zip(zipFile, charset, withSrcDir, srcFile);
		return zipFile;
	}

	/**
	 * 对文件或文件目录进行压缩<br>
	 * 使用默认UTF-8编码
	 *
	 * @param zipFile    生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩
	 * @param srcFiles   要压缩的源文件或目录。
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(File zipFile, boolean withSrcDir, File... srcFiles) throws UtilException {
		return zip(zipFile, DEFAULT_CHARSET, withSrcDir, srcFiles);
	}

	/**
	 * 对文件或文件目录进行压缩
	 *
	 * @param zipFile    生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param charset    编码
	 * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩
	 * @param srcFiles   要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 */
	public static File zip(File zipFile, Charset charset, boolean withSrcDir, File... srcFiles) throws UtilException {
		return zip(zipFile, charset, withSrcDir, null, srcFiles);
	}

	/**
	 * 对文件或文件目录进行压缩
	 *
	 * @param zipFile    生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param charset    编码
	 * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩
	 * @param filter     文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）
	 * @param srcFiles   要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
	 * @return 压缩文件
	 * @throws IORuntimeException IO异常
	 * @since 4.6.5
	 */
	public static File zip(File zipFile, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException {
		validateFiles(zipFile, srcFiles);
		//noinspection resource
		ZipWriter.of(zipFile, charset).add(withSrcDir, filter, srcFiles).close();
		return zipFile;
	}

	/**
	 * 对文件或文件目录进行压缩
	 *
	 * @param out        生成的Zip到的目标流，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param charset    编码
	 * @param withSrcDir 是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩
	 * @param filter     文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）
	 * @param srcFiles   要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
	 * @throws IORuntimeException IO异常
	 * @since 5.1.1
	 */
	public static void zip(OutputStream out, Charset charset, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException {
		ZipWriter.of(out, charset).add(withSrcDir, filter, srcFiles).close();
	}

	/**
	 * 对文件或文件目录进行压缩
	 *
	 * @param zipOutputStream 生成的Zip到的目标流，自动关闭此流
	 * @param withSrcDir      是否包含被打包目录，只针对压缩目录有效。若为false，则只压缩目录下的文件或目录，为true则将本目录也压缩
	 * @param filter          文件过滤器，通过实现此接口，自定义要过滤的文件（过滤掉哪些文件或文件夹不加入压缩）
	 * @param srcFiles        要压缩的源文件或目录。如果压缩一个文件，则为该文件的全路径；如果压缩一个目录，则为该目录的顶层目录路径
	 * @throws IORuntimeException IO异常
	 * @since 5.1.1
	 * @deprecated 请使用 {@link #zip(OutputStream, Charset, boolean, FileFilter, File...)}
	 */
	@Deprecated
	public static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles) throws IORuntimeException {
		try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) {
			zipWriter.add(withSrcDir, filter, srcFiles);
		}
	}

	/**
	 * 对流中的数据加入到压缩文件，使用默认UTF-8编码
	 *
	 * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param path    流数据在压缩文件中的路径或文件名
	 * @param data    要压缩的数据
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 * @since 3.0.6
	 */
	public static File zip(File zipFile, String path, String data) throws UtilException {
		return zip(zipFile, path, data, DEFAULT_CHARSET);
	}

	/**
	 * 对流中的数据加入到压缩文件<br>
	 *
	 * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param path    流数据在压缩文件中的路径或文件名
	 * @param data    要压缩的数据
	 * @param charset 编码
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 * @since 3.2.2
	 */
	public static File zip(File zipFile, String path, String data, Charset charset) throws UtilException {
		return zip(zipFile, path, IoUtil.toStream(data, charset), charset);
	}

	/**
	 * 对流中的数据加入到压缩文件<br>
	 * 使用默认编码UTF-8
	 *
	 * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param path    流数据在压缩文件中的路径或文件名
	 * @param in      要压缩的源
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 * @since 3.0.6
	 */
	public static File zip(File zipFile, String path, InputStream in) throws UtilException {
		return zip(zipFile, path, in, DEFAULT_CHARSET);
	}

	/**
	 * 对流中的数据加入到压缩文件
	 *
	 * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param path    流数据在压缩文件中的路径或文件名
	 * @param in      要压缩的源，默认关闭
	 * @param charset 编码
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 * @since 3.2.2
	 */
	public static File zip(File zipFile, String path, InputStream in, Charset charset) throws UtilException {
		return zip(zipFile, new String[]{path}, new InputStream[]{in}, charset);
	}

	/**
	 * 对流中的数据加入到压缩文件<br>
	 * 路径列表和流列表长度必须一致
	 *
	 * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param paths   流数据在压缩文件中的路径或文件名
	 * @param ins     要压缩的源，添加完成后自动关闭流
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 * @since 3.0.9
	 */
	public static File zip(File zipFile, String[] paths, InputStream[] ins) throws UtilException {
		return zip(zipFile, paths, ins, DEFAULT_CHARSET);
	}

	/**
	 * 对流中的数据加入到压缩文件<br>
	 * 路径列表和流列表长度必须一致
	 *
	 * @param zipFile 生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param paths   流数据在压缩文件中的路径或文件名
	 * @param ins     要压缩的源，添加完成后自动关闭流
	 * @param charset 编码
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 * @since 3.0.9
	 */
	public static File zip(File zipFile, String[] paths, InputStream[] ins, Charset charset) throws UtilException {
		try (final ZipWriter zipWriter = ZipWriter.of(zipFile, charset)) {
			zipWriter.add(paths, ins);
		}

		return zipFile;
	}

	/**
	 * 将文件流压缩到目标流中
	 *
	 * @param out   目标流，压缩完成自动关闭
	 * @param paths 流数据在压缩文件中的路径或文件名
	 * @param ins   要压缩的源，添加完成后自动关闭流
	 * @since 5.5.2
	 */
	public static void zip(OutputStream out, String[] paths, InputStream[] ins) {
		zip(getZipOutputStream(out, DEFAULT_CHARSET), paths, ins);
	}

	/**
	 * 将文件流压缩到目标流中
	 *
	 * @param zipOutputStream 目标流，压缩完成自动关闭
	 * @param paths           流数据在压缩文件中的路径或文件名
	 * @param ins             要压缩的源，添加完成后自动关闭流
	 * @throws IORuntimeException IO异常
	 * @since 5.5.2
	 */
	public static void zip(ZipOutputStream zipOutputStream, String[] paths, InputStream[] ins) throws IORuntimeException {
		try (final ZipWriter zipWriter = new ZipWriter(zipOutputStream)) {
			zipWriter.add(paths, ins);
		}
	}

	/**
	 * 对流中的数据加入到压缩文件<br>
	 * 路径列表和流列表长度必须一致
	 *
	 * @param zipFile   生成的Zip文件，包括文件名。注意：zipPath不能是srcPath路径下的子文件夹
	 * @param charset   编码
	 * @param resources 需要压缩的资源，资源的路径为{@link Resource#getName()}
	 * @return 压缩文件
	 * @throws UtilException IO异常
	 * @since 5.5.2
	 */
	public static File zip(File zipFile, Charset charset, Resource... resources) throws UtilException {
		//noinspection resource
		ZipWriter.of(zipFile, charset).add(resources).close();
		return zipFile;
	}

	// ---------------------------------------------------------------------------------------------- Unzip

	/**
	 * 解压到文件名相同的目录中，默认编码UTF-8
	 *
	 * @param zipFilePath 压缩文件路径
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 */
	public static File unzip(String zipFilePath) throws UtilException {
		return unzip(zipFilePath, DEFAULT_CHARSET);
	}

	/**
	 * 解压到文件名相同的目录中
	 *
	 * @param zipFilePath 压缩文件路径
	 * @param charset     编码
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 * @since 3.2.2
	 */
	public static File unzip(String zipFilePath, Charset charset) throws UtilException {
		return unzip(FileUtil.file(zipFilePath), charset);
	}

	/**
	 * 解压到文件名相同的目录中，使用UTF-8编码
	 *
	 * @param zipFile 压缩文件
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 * @since 3.2.2
	 */
	public static File unzip(File zipFile) throws UtilException {
		return unzip(zipFile, DEFAULT_CHARSET);
	}

	/**
	 * 解压到文件名相同的目录中
	 *
	 * @param zipFile 压缩文件
	 * @param charset 编码
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 * @since 3.2.2
	 */
	public static File unzip(File zipFile, Charset charset) throws UtilException {
		final File destDir = FileUtil.file(zipFile.getParentFile(), FileUtil.mainName(zipFile));
		return unzip(zipFile, destDir, charset);
	}

	/**
	 * 解压，默认UTF-8编码
	 *
	 * @param zipFilePath 压缩文件的路径
	 * @param outFileDir  解压到的目录
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 */
	public static File unzip(String zipFilePath, String outFileDir) throws UtilException {
		return unzip(zipFilePath, outFileDir, DEFAULT_CHARSET);
	}

	/**
	 * 解压
	 *
	 * @param zipFilePath 压缩文件的路径
	 * @param outFileDir  解压到的目录
	 * @param charset     编码
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 */
	public static File unzip(String zipFilePath, String outFileDir, Charset charset) throws UtilException {
		return unzip(FileUtil.file(zipFilePath), FileUtil.mkdir(outFileDir), charset);
	}

	/**
	 * 解压，默认使用UTF-8编码
	 *
	 * @param zipFile zip文件
	 * @param outFile 解压到的目录
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 */
	public static File unzip(File zipFile, File outFile) throws UtilException {
		return unzip(zipFile, outFile, DEFAULT_CHARSET);
	}

	/**
	 * 解压
	 *
	 * @param zipFile zip文件
	 * @param outFile 解压到的目录
	 * @param charset 编码
	 * @return 解压的目录
	 * @since 3.2.2
	 */
	public static File unzip(File zipFile, File outFile, Charset charset) {
		return unzip(toZipFile(zipFile, charset), outFile);
	}

	/**
	 * 解压
	 *
	 * @param zipFile zip文件，附带编码信息，使用完毕自动关闭
	 * @param outFile 解压到的目录
	 * @return 解压的目录
	 * @throws IORuntimeException IO异常
	 * @since 4.5.8
	 */
	public static File unzip(ZipFile zipFile, File outFile) throws IORuntimeException {
		return unzip(zipFile, outFile, -1);
	}

	/**
	 * 限制解压后文件大小
	 *
	 * @param zipFile zip文件，附带编码信息，使用完毕自动关闭
	 * @param outFile 解压到的目录
	 * @param limit   限制解压文件大小(单位B)
	 * @return 解压的目录
	 * @throws IORuntimeException IO异常
	 * @since 5.8.5
	 */
	public static File unzip(ZipFile zipFile, File outFile, long limit) throws IORuntimeException {
		if (outFile.exists() && outFile.isFile()) {
			throw new IllegalArgumentException(
					StrUtil.format("Target path [{}] exist!", outFile.getAbsolutePath()));
		}

		// pr#726@Gitee
		if (limit > 0) {
			final Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
			long zipFileSize = 0L;
			while (zipEntries.hasMoreElements()) {
				final ZipEntry zipEntry = zipEntries.nextElement();
				zipFileSize += zipEntry.getSize();
				if (zipFileSize > limit) {
					throw new IllegalArgumentException("The file size exceeds the limit");
				}
			}
		}

		try (final ZipReader reader = new ZipReader(zipFile)) {
			reader.readTo(outFile);
		}
		return outFile;
	}

	/**
	 * 获取压缩包中的指定文件流
	 *
	 * @param zipFile 压缩文件
	 * @param charset 编码
	 * @param path    需要提取文件的文件名或路径
	 * @return 压缩文件流，如果未找到返回{@code null}
	 * @since 5.5.2
	 */
	public static InputStream get(File zipFile, Charset charset, String path) {
		return get(toZipFile(zipFile, charset), path);
	}

	/**
	 * 获取压缩包中的指定文件流
	 *
	 * @param zipFile 压缩文件
	 * @param path    需要提取文件的文件名或路径
	 * @return 压缩文件流，如果未找到返回{@code null}
	 * @since 5.5.2
	 */
	public static InputStream get(ZipFile zipFile, String path) {
		final ZipEntry entry = zipFile.getEntry(path);
		if (null != entry) {
			return getStream(zipFile, entry);
		}
		return null;
	}

	/**
	 * 读取并处理Zip文件中的每一个{@link ZipEntry}
	 *
	 * @param zipFile  Zip文件
	 * @param consumer {@link ZipEntry}处理器
	 * @since 5.5.2
	 */
	public static void read(ZipFile zipFile, Consumer<ZipEntry> consumer) {
		try (final ZipReader reader = new ZipReader(zipFile)) {
			reader.read(consumer);
		}
	}

	/**
	 * 解压<br>
	 * ZIP条目不使用高速缓冲。
	 *
	 * @param in      zip文件流，使用完毕自动关闭
	 * @param outFile 解压到的目录
	 * @param charset 编码
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 * @since 4.5.8
	 */
	public static File unzip(InputStream in, File outFile, Charset charset) throws UtilException {
		if (null == charset) {
			charset = DEFAULT_CHARSET;
		}
		return unzip(new ZipInputStream(in, charset), outFile);
	}

	/**
	 * 解压<br>
	 * ZIP条目不使用高速缓冲。
	 *
	 * @param zipStream zip文件流，包含编码信息
	 * @param outFile   解压到的目录
	 * @return 解压的目录
	 * @throws UtilException IO异常
	 * @since 4.5.8
	 */
	public static File unzip(ZipInputStream zipStream, File outFile) throws UtilException {
		try (final ZipReader reader = new ZipReader(zipStream)) {
			reader.readTo(outFile);
		}
		return outFile;
	}

	/**
	 * 读取并处理Zip流中的每一个{@link ZipEntry}
	 *
	 * @param zipStream zip文件流，包含编码信息
	 * @param consumer  {@link ZipEntry}处理器
	 * @since 5.5.2
	 */
	public static void read(ZipInputStream zipStream, Consumer<ZipEntry> consumer) {
		try (final ZipReader reader = new ZipReader(zipStream)) {
			reader.read(consumer);
		}
	}

	/**
	 * 从Zip文件中提取指定的文件为bytes
	 *
	 * @param zipFilePath Zip文件
	 * @param name        文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt
	 * @return 文件内容bytes
	 * @since 4.1.8
	 */
	public static byte[] unzipFileBytes(String zipFilePath, String name) {
		return unzipFileBytes(zipFilePath, DEFAULT_CHARSET, name);
	}

	/**
	 * 从Zip文件中提取指定的文件为bytes
	 *
	 * @param zipFilePath Zip文件
	 * @param charset     编码
	 * @param name        文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt
	 * @return 文件内容bytes
	 * @since 4.1.8
	 */
	public static byte[] unzipFileBytes(String zipFilePath, Charset charset, String name) {
		return unzipFileBytes(FileUtil.file(zipFilePath), charset, name);
	}

	/**
	 * 从Zip文件中提取指定的文件为bytes
	 *
	 * @param zipFile Zip文件
	 * @param name    文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt
	 * @return 文件内容bytes
	 * @since 4.1.8
	 */
	public static byte[] unzipFileBytes(File zipFile, String name) {
		return unzipFileBytes(zipFile, DEFAULT_CHARSET, name);
	}

	/**
	 * 从Zip文件中提取指定的文件为bytes
	 *
	 * @param zipFile Zip文件
	 * @param charset 编码
	 * @param name    文件名，如果存在于子文件夹中，此文件名必须包含目录名，例如images/aaa.txt
	 * @return 文件内容bytes
	 * @since 4.1.8
	 */
	public static byte[] unzipFileBytes(File zipFile, Charset charset, String name) {
		try (final ZipReader reader = ZipReader.of(zipFile, charset)) {
			return IoUtil.readBytes(reader.get(name));
		}
	}

	// ----------------------------------------------------------------------------- Gzip

	/**
	 * Gzip压缩处理
	 *
	 * @param content 被压缩的字符串
	 * @param charset 编码
	 * @return 压缩后的字节流
	 * @throws UtilException IO异常
	 */
	public static byte[] gzip(String content, String charset) throws UtilException {
		return gzip(StrUtil.bytes(content, charset));
	}

	/**
	 * Gzip压缩处理
	 *
	 * @param buf 被压缩的字节流
	 * @return 压缩后的字节流
	 * @throws UtilException IO异常
	 */
	public static byte[] gzip(byte[] buf) throws UtilException {
		return gzip(new ByteArrayInputStream(buf), buf.length);
	}

	/**
	 * Gzip压缩文件
	 *
	 * @param file 被压缩的文件
	 * @return 压缩后的字节流
	 * @throws UtilException IO异常
	 */
	public static byte[] gzip(File file) throws UtilException {
		BufferedInputStream in = null;
		try {
			in = FileUtil.getInputStream(file);
			return gzip(in, (int) file.length());
		} finally {
			IoUtil.close(in);
		}
	}

	/**
	 * Gzip压缩文件
	 *
	 * @param in 被压缩的流
	 * @return 压缩后的字节流
	 * @throws UtilException IO异常
	 * @since 4.1.18
	 */
	public static byte[] gzip(InputStream in) throws UtilException {
		return gzip(in, DEFAULT_BYTE_ARRAY_LENGTH);
	}

	/**
	 * Gzip压缩文件
	 *
	 * @param in     被压缩的流
	 * @param length 预估长度
	 * @return 压缩后的字节流
	 * @throws UtilException IO异常
	 * @since 4.1.18
	 */
	public static byte[] gzip(InputStream in, int length) throws UtilException {
		final ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
		Gzip.of(in, bos).gzip().close();
		return bos.toByteArray();
	}

	/**
	 * Gzip解压缩处理
	 *
	 * @param buf     压缩过的字节流
	 * @param charset 编码
	 * @return 解压后的字符串
	 * @throws UtilException IO异常
	 */
	public static String unGzip(byte[] buf, String charset) throws UtilException {
		return StrUtil.str(unGzip(buf), charset);
	}

	/**
	 * Gzip解压处理
	 *
	 * @param buf buf
	 * @return bytes
	 * @throws UtilException IO异常
	 */
	public static byte[] unGzip(byte[] buf) throws UtilException {
		return unGzip(new ByteArrayInputStream(buf), buf.length);
	}

	/**
	 * Gzip解压处理
	 *
	 * @param in Gzip数据
	 * @return 解压后的数据
	 * @throws UtilException IO异常
	 */
	public static byte[] unGzip(InputStream in) throws UtilException {
		return unGzip(in, DEFAULT_BYTE_ARRAY_LENGTH);
	}

	/**
	 * Gzip解压处理
	 *
	 * @param in     Gzip数据
	 * @param length 估算长度，如果无法确定请传入{@link #DEFAULT_BYTE_ARRAY_LENGTH}
	 * @return 解压后的数据
	 * @throws UtilException IO异常
	 * @since 4.1.18
	 */
	public static byte[] unGzip(InputStream in, int length) throws UtilException {
		FastByteArrayOutputStream bos = new FastByteArrayOutputStream(length);
		Gzip.of(in, bos).unGzip().close();
		return bos.toByteArray();
	}

	// ----------------------------------------------------------------------------- Zlib

	/**
	 * Zlib压缩处理
	 *
	 * @param content 被压缩的字符串
	 * @param charset 编码
	 * @param level   压缩级别，1~9
	 * @return 压缩后的字节流
	 * @since 4.1.4
	 */
	public static byte[] zlib(String content, String charset, int level) {
		return zlib(StrUtil.bytes(content, charset), level);
	}

	/**
	 * Zlib压缩文件
	 *
	 * @param file  被压缩的文件
	 * @param level 压缩级别
	 * @return 压缩后的字节流
	 * @since 4.1.4
	 */
	public static byte[] zlib(File file, int level) {
		BufferedInputStream in = null;
		try {
			in = FileUtil.getInputStream(file);
			return zlib(in, level, (int) file.length());
		} finally {
			IoUtil.close(in);
		}
	}

	/**
	 * 打成Zlib压缩包
	 *
	 * @param buf   数据
	 * @param level 压缩级别，0~9
	 * @return 压缩后的bytes
	 * @since 4.1.4
	 */
	public static byte[] zlib(byte[] buf, int level) {
		return zlib(new ByteArrayInputStream(buf), level, buf.length);
	}

	/**
	 * 打成Zlib压缩包
	 *
	 * @param in    数据流
	 * @param level 压缩级别，0~9
	 * @return 压缩后的bytes
	 * @since 4.1.19
	 */
	public static byte[] zlib(InputStream in, int level) {
		return zlib(in, level, DEFAULT_BYTE_ARRAY_LENGTH);
	}

	/**
	 * 打成Zlib压缩包
	 *
	 * @param in     数据流
	 * @param level  压缩级别，0~9
	 * @param length 预估大小
	 * @return 压缩后的bytes
	 * @since 4.1.19
	 */
	public static byte[] zlib(InputStream in, int level, int length) {
		final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
		Deflate.of(in, out, false).deflater(level);
		return out.toByteArray();
	}

	/**
	 * Zlib解压缩处理
	 *
	 * @param buf     压缩过的字节流
	 * @param charset 编码
	 * @return 解压后的字符串
	 * @since 4.1.4
	 */
	public static String unZlib(byte[] buf, String charset) {
		return StrUtil.str(unZlib(buf), charset);
	}

	/**
	 * 解压缩zlib
	 *
	 * @param buf 数据
	 * @return 解压后的bytes
	 * @since 4.1.4
	 */
	public static byte[] unZlib(byte[] buf) {
		return unZlib(new ByteArrayInputStream(buf), buf.length);
	}

	/**
	 * 解压缩zlib
	 *
	 * @param in 数据流
	 * @return 解压后的bytes
	 * @since 4.1.19
	 */
	public static byte[] unZlib(InputStream in) {
		return unZlib(in, DEFAULT_BYTE_ARRAY_LENGTH);
	}

	/**
	 * 解压缩zlib
	 *
	 * @param in     数据流
	 * @param length 预估长度
	 * @return 解压后的bytes
	 * @since 4.1.19
	 */
	public static byte[] unZlib(InputStream in, int length) {
		final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
		Deflate.of(in, out, false).inflater();
		return out.toByteArray();
	}

	/**
	 * 获取Zip文件中指定目录下的所有文件，只显示文件，不显示目录<br>
	 * 此方法并不会关闭{@link ZipFile}。
	 *
	 * @param zipFile Zip文件
	 * @param dir     目录前缀（目录前缀不包含开头的/）
	 * @return 文件列表
	 * @since 4.6.6
	 */
	public static List<String> listFileNames(ZipFile zipFile, String dir) {
		if (StrUtil.isNotBlank(dir)) {
			// 目录尾部添加"/"
			dir = StrUtil.addSuffixIfNot(dir, StrUtil.SLASH);
		}

		final List<String> fileNames = new ArrayList<>();
		String name;
		for (ZipEntry entry : new EnumerationIter<>(zipFile.entries())) {
			name = entry.getName();
			if (StrUtil.isEmpty(dir) || name.startsWith(dir)) {
				final String nameSuffix = StrUtil.removePrefix(name, dir);
				if (StrUtil.isNotEmpty(nameSuffix) && false == StrUtil.contains(nameSuffix, CharUtil.SLASH)) {
					fileNames.add(nameSuffix);
				}
			}
		}

		return fileNames;
	}

	// ---------------------------------------------------------------------------------------------- Private method start

	/**
	 * 判断压缩文件保存的路径是否为源文件路径的子文件夹，如果是，则抛出异常（防止无限递归压缩的发生）
	 *
	 * @param zipFile  压缩后的产生的文件路径
	 * @param srcFiles 被压缩的文件或目录
	 */
	private static void validateFiles(File zipFile, File... srcFiles) throws UtilException {
		if (zipFile.isDirectory()) {
			throw new UtilException("Zip file [{}] must not be a directory !", zipFile.getAbsoluteFile());
		}

		for (File srcFile : srcFiles) {
			if (null == srcFile) {
				continue;
			}
			if (false == srcFile.exists()) {
				throw new UtilException(StrUtil.format("File [{}] not exist!", srcFile.getAbsolutePath()));
			}

			// issue#1961@Github
			// 当 zipFile =  new File("temp.zip") 时, zipFile.getParentFile() == null
			File parentFile;
			try {
				parentFile = zipFile.getCanonicalFile().getParentFile();
			} catch (IOException e) {
				parentFile = zipFile.getParentFile();
			}

			// 压缩文件不能位于被压缩的目录内
			if (srcFile.isDirectory() && FileUtil.isSub(srcFile, parentFile)) {
				throw new UtilException("Zip file path [{}] must not be the child directory of [{}] !", zipFile.getPath(), srcFile.getPath());
			}
		}
	}
	// ---------------------------------------------------------------------------------------------- Private method end

}
