读取文件条目名称时出现 java.io.UTFDataFormatException

问题描述

我正在尝试使用 DataInputStream / DataOutputStream 将多个文件(以前在 jar 存档中)“打包”到另一个非 jar 文件中。

这个想法是:

datatable(label:string,data:string) [ 
    'First','First=abc,Second=def,Third=ghi','Second',Third= ghi','Third',Third= ghi'
]
| project label,data = split(data,',')
| mv-expand bagexpansion=array data to typeof(string)
| project label,'=')
| where label == trim(' ',tostring(data[0]))
| project label,value = trim(' ',tostring(data[1]))

代码

    First int = number of entries
    
    First UTF is the first entry name
    
    Second Int is entry byte array length (entry size)

    Then repeat for every entry.

拆开原包装到新包装:

 public static void main(String[] args) throws Throwable {
        test();

        System.out.println("========================================================================================");

        final DataInputStream dataInputStream = new DataInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJarOut")));

        for (int int1 = dataInputStream.readInt(),i = 0; i < int1; ++i) {
            final String utf = dataInputStream.readUTF();
            System.out.println("Entry name: " + utf);
            final byte[] array = new byte[dataInputStream.readInt()];
            for (int j = 0; j < array.length; ++j) {
                array[j] = dataInputStream.readByte();
            }
            System.out.println("Entry bytes length: " + array.length);
        }

    }

显然我可以毫无问题地打开第一个条目,第二个条目和其他条目:

private static void test() throws Throwable {
    JarInputStream stream = new JarInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJar.jar")));
    JarInputStream stream1 = new JarInputStream(new FileInputStream(new File("C:\\Users\\Admin\\Desktop\\randomJar.jar")));

    final byte[] buffer = new byte[2048];
    final DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(new File("C:\\Users\\Admin\\Desktop\\randomJarOut")));

    int entryCount = 0;
    for (ZipEntry entry; (entry = stream.getNextJarEntry()) != null; ) {
        entryCount++;
    }

    outputStream.writeInt(entryCount);

    for (JarEntry entry; (entry = stream1.getNextJarEntry()) != null; ) {
        int entryRealSize = stream1.read(buffer);
        if (!(entryRealSize == -1)) {
            System.out.println("Writing: " + entry.getName() + " Length: " + entryRealSize);

            outputStream.writeUTF(entry.getName());
            outputStream.writeInt(entryRealSize);

            for (int len = stream1.read(buffer); len != -1; len = stream1.read(buffer)) {
                outputStream.write(buffer,len);
            }
        }
    }
    outputStream.flush();
    outputStream.close();
}

有谁知道如何解决这个问题?为什么这适用于第一个条目而不适用于其他条目?

解决方法

问题可能在于您混合了不互惠的读/写方法:

  1. writer 方法使用 outputStream.writeInt(entryCount) 写入,而 main 方法使用 dataInputStream.readInt() 读取。没关系。
  2. writer 方法使用 outputStream.writeUTF(entry.getName()) 写入,而 main 方法使用 dataInputStream.readUTF() 读取。没关系。
  3. writer 方法使用 outputStream.writeInt(entryRealSize) 写入,而 main 方法使用 dataInputStream.readInt() 读取。没关系。
  4. writer 方法使用 outputStream.write(buffer,len) 写入,而 main 方法使用 dataInputStream.readByte() 读取多次。错了。

如果你用 write(buffer,offset,len) 写入一个字节数组,你必须用 read(buffer,len) 读取它,因为 write(buffer,len) 正好将 len 物理字节写入输出流,而 { {1}}(writeByte 的对应物)写入了大量关于对象类型的元数据开销,然后是它的状态变量。

编写器方法中的错误

writer 方法中还有一个主要错误:它最多调用 3 次 readByte,但它只使用了一次 stream1.read(buffer) 内容。结果是文件的实际大小实际上写入了输出流元数据中,但后面只是一小部分数据。

如果您需要在将其写入输出流之前知道输入文件的大小,您有两种选择:

  • 要么选择足够大的缓冲区大小(如 204800),这样您只需一次读取即可读取整个文件,并只需一次写入即可写入。
  • 或者单独的读/写算法:首先是一种读取整个文件并将其存储在内存中的方法(例如,一个字节 []),然后是另一种将字节 [] 写入输出流的方法。

完全固定的解决方案

我已经修复了您的程序,为每个任务提供了特定的、分离的方法。该过程包括将输入文件解析为内存模型,根据您的自定义定义将其写入中间文件,然后将其读回。

buffer
,

我的看法是 jar 文件(实际上是一个 zip 文件)有一个 Central Directory,它只能用 ZipFile(或 JarFile)类读取。 中央目录包含有关条目的一些数据,例如大小。

我认为 ZipInputStream 不会读取中央目录,因此 ZipEntry 不会包含大小(返回 -1,因为它是未知的),而从 ZipFile 类读取 ZipEntry 会。

因此,如果您首先使用 ZipFile 读取每个条目的大小并将其存储在地图中,则可以在使用 ZipInputStream 读取数据时轻松获取它。

This page 也包括一些很好的例子。

所以我的代码版本是:

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

public class JarRepacker {

    public static void main(String[] args) throws Throwable {
        JarRepacker repacker = new JarRepacker();
        repacker.repackJarToMyFileFormat("commons-cli-1.3.1.jar","randomJarOut.bin");
        repacker.readMyFileFormat("randomJarOut.bin");
    }
    
    private void repackJarToMyFileFormat(String inputJar,String outputFile) throws Throwable {
        int entryCount;
        Map<String,Integer> sizeMap = new HashMap<>();
        try (ZipFile zipFile = new ZipFile(inputJar)) {
            entryCount = zipFile.size();
            zipFile.entries().asIterator().forEachRemaining(e -> sizeMap.put(e.getName(),(int) e.getSize()));
        }

        try (final DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(outputFile))) {

            outputStream.writeInt(entryCount);

            try (ZipInputStream stream = new ZipInputStream(new BufferedInputStream(new FileInputStream(inputJar)))) {
                ZipEntry entry;
                final byte[] buffer = new byte[2048];
                while ((entry = stream.getNextEntry()) != null) {
                    final String name = entry.getName();
                    outputStream.writeUTF(name);
                    final Integer size = sizeMap.get(name);
                    outputStream.writeInt(size);
                    //System.out.println("Writing: " + name + " Size: " + size);

                    int len;
                    while ((len = stream.read(buffer)) > 0) {
                        outputStream.write(buffer,len);
                    }
                }
            }
            outputStream.flush();
        }
    }

    private void readMyFileFormat(String fileToRead) throws IOException {
        try (DataInputStream dataInputStream
                     = new DataInputStream(new BufferedInputStream(new FileInputStream(fileToRead)))) {

            int entries = dataInputStream.readInt();
            System.out.println("Entries in file: " + entries);
            for (int i = 1; i <= entries; i++) {
                final String name = dataInputStream.readUTF();
                final int size = dataInputStream.readInt();
                System.out.printf("[%3d] Reading: %s of size: %d%n",i,name,size);
                final byte[] array = new byte[size];
                for (int j = 0; j < array.length; ++j) {
                    array[j] = dataInputStream.readByte();
                }
                // Still need to do something with this array...
            }
        }
    }

}