问题
The ColdFusion application that I support is installed at two different locations. One location is running Windows Server 2008 with ColdFusion 9 and MS SQL Server 2008 and the other is running Windows Server 2012 with ColdFusion 11 and MS SQL Server 2012. The application provides an export process, using CFZIP action = "zip"
and then the zip file is imported on the destination machine, using CFZIP action = "unzip"
.
Here is the code I have to generate the zip file for export:
<cfzip file="exportFileName.zip"
source="#exportDirectory#"
action="zip"
overwrite="yes"
recurse="yes">
The .zip file gets generated properly and I can open it in Windows Explorer and 7Zip with no issues.
Here is the code to unzip the zip file created above:
<cfzip action="unzip"
file="exportFileName.zip"
destination="#destination#\xml"
recurse ="yes"
storepath="yes">
This is the same code on both the ColdFusion 9 and ColdFusion 11 instances, however when we try to unzip a zip file generated by the ColdFusion 11 instance on the ColdFusion 9 instance I receive the following error:
Ensure that the file is a valid zip file and it is accessible. Cause: java.util.zip.ZipException: only DEFLATED entries can have EXT descriptor
We're only seeing this issue when using CFZIP
on a ColdFusion 9 server to unzip a zip file generated using CFZIP
on a ColdFusion 11 server. I can extract the contents of the zip file generated from ColdFusion 11 on the ColdFusion 9 server, create a new Zip file, using 7Zip, that contains the exact contents of the original ColdFusion 11-generated zip file, and I don't get the error.
The process works fine whenever we test it going from a ColdFusion 11 source to a ColdFusion 11 destination or from a ColdFusion 9 source to a ColdFusion 9 destination. We're only getting the issue when zipping on ColdFusion 11 and trying to unzip on ColdFusion 9. I've scoured Google, but can't seem to find any issues like this. Any help would be greatly appreciated.
回答1:
[TL/DR]
CFZip
on ColdFusion 11 complies with a zip specification later than PKZIP 2.04g- Java's
ZipInputStream
(which CF9 appears to use) complies strictly to the PKZIP 2.04g (or earlier) specification.
A subtle difference between the two versions is their handling of zero-length entries (i.e. sub-directories or mime-types among others). CFZip
in CF11 marks these entries as being STORED
with no compression (which, intuitively and by later versions of the specification, is fine as they are zero-length so compression won't do anything to them) but ZipInputStream
(called by CFZip
in CF9) expects them to be marked as compressed using the DEFLATE
method.
If you have no zero-length entries then the files generated is ColdFusion may be able to be read (I think they will but I can't prove it definitively) using java's ZipInputStream
; however, if you have zero-length files then it will throw errors.
Alternatively, either:
- Use something other than
CFZip
in CF9 which is able to read zip files compliant against later standards (i.e. you could usecfexecute
to run 7zip external to ColdFusion or use the javaorg.apache.commons.compress
library inside ColdFusion); or - Use something other than
CFZip
in CF11 which will restrict the zip files it creates to the earlier standard (see below).
Detailed answer:
From the Zip Format Specification section 4.4.4 general purpose bit flag:
Bit 3: If this bit is set, the fields crc-32, compressed size and uncompressed size are set to zero in the local header. The correct values are put in the data descriptor immediately following the compressed data. (Note: PKZIP version 2.04g for DOS only recognizes this bit for method 8 compression, newer versions of PKZIP recognize this bit for any compression method.)
This bit is set when the local file header indicates that there is a zero-length entry in the zip file (i.e. a directory or certain other things in a zip-file like an embedded mime-type).
Under the PKZIP version 2.04g (or earlier) standard it would then expect the compression method flag to be set to DEFLATE
(method 8 compression). Java's ZipInputStream
complies strictly to this standard and throws a ZipException
(with the message only DEFLATED entries can have EXT descriptor
) if it does not find this compression method (see source code here).
cfzip
appears to set the compression method to STORED
(method 0 compression - or no compression) when the General Purpose Flag bit is set. From what I have read this complies with later versions of the PKZIP standard but is not backwards compatible with the version of the standard implemented by Java in the ZipInputStream
class.
How to mitigate for this:
- Either don't store zero-length files in the zip (which means I don't think you can use the
recurse
option inCFZip
); or - Don't use
CFZip
, in CF11, if you want strict backwards compatability and use something else (see below for a suggestion). - Use
CFZip
in CF11 but use something else to unzip the files in CF9 that is compatible with a greater range of compression algorithms (i.e. usecfexecute
to call an external program to handle the zip files; use the javaorg.apache.commons.compress
library; etc).
If you go for option 2 and want to use something compatible with the eariler versions then this worked for me and I can unzip the files using ZipInputStream
(tested directly in Java as I don't have CF9):
package zip;
import java.io.*;
import java.util.zip.*;
public class Zip {
private static void processFolder(
final File folder,
final ZipOutputStream zos,
final boolean recurse,
final int prefixLength,
final byte[] buffer
)
throws IOException
{
for ( final File file : folder.listFiles() )
{
if ( file.isFile() )
{
final String name = file.getPath().substring( prefixLength );
// System.out.println( name );
final ZipEntry entry = new ZipEntry( name );
zos.putNextEntry(entry);
try (FileInputStream is = new FileInputStream( file ) ){
int read;
while( (read = is.read( buffer ) ) != -1 )
{
zos.write( buffer, 0, read );
}
}
zos.closeEntry();
}
else if ( recurse && file.isDirectory() )
{
processFolder( file, zos, recurse, prefixLength, buffer );
}
}
}
public static void zipFolder(
final String folderPath,
final String outputName,
final boolean recurse,
final boolean overwrite
) throws IOException
{
final File folder = new File( folderPath );
if ( folder.exists() && folder.isDirectory() ) {
final File output = new File( outputName );
if ( overwrite || !output.exists() )
{
try ( ZipOutputStream zos = new ZipOutputStream( new FileOutputStream( output ) ) )
{
processFolder( folder, zos, recurse, folder.getPath().length() + 1, new byte[1024*4] );
}
}
}
}
}
Based on this answer
(Note: the error handling is minimal so you probably want to make this more robust if you are going to use it in a production environment).
If you compile that java class and put the .class
file in a zip
subdirectory on your class path (which you can add entries to in the ColdFusion Administration panel).
Then you can call it using:
<cfscript>
zip = CreateObject( "java", zip.Zip" );
zip.zipFolder(
"/path/to/folder/to/be/zipped/",
"/path/to/output/zip/file.zip",
true, // recurse
true // overwrite existing file
);
</cfscript>
Testing
If you want something to test how this occurs then this will generate a valid zip file under the later specifiction which will generate the error:
import java.io.*;
import java.util.zip.*;
public class TestZip {
public static void main( final String[] args ) throws IOException {
final File file = new File( args[0] );
if ( file.exists() )
{
System.out.println( "File already exists" );
return;
}
final boolean general_purpose_bit_flag_bit3_on = true;
final byte gpbf = general_purpose_bit_flag_bit3_on ? 0x08 : 0x00;
final byte[] contents = new byte[]{
// Local File header
'P', 'K', 3, 4, // Local File Header Signature
13, 0, // Version needed to extract
gpbf, 8, // General purpose bit flag
ZipEntry.STORED, 0, // Compression method
'q', 'l', 't', 'G', // Last Modification time & date
0, 0, 0, 0, // CRC32
0, 0, 0, 0, // Compressed Size
0, 0, 0, 0, // Uncompressed Size
12, 0, // File name length
0, 0, // Extra field length
'F', 'o', 'l', 'd', 'e', 'r', '_', 'n', 'a', 'm', 'e', '/',
// File name
// Central directory file header
'P', 'K', 1, 2, // Central Directory File Header Signature
13, 0, // Version made by
13, 0, // Version needed to extract
gpbf, 8, // General purpose bit flag
ZipEntry.STORED, 0, // Compression method
'q', 'l', 't', 'G', // Last Modification time & date
0, 0, 0, 0, // CRC32
0, 0, 0, 0, // Compressed Size
0, 0, 0, 0, // Uncompressed Size
12, 0, // File name length
0, 0, // Extra field length
0, 0, // File comment length
0, 0, // Disk number where file starts
0, 0, // Internal File attributes
0, 0, 0, 0, // External File attributes
0, 0, 0, 0, // Relative offset of local header file
'F', 'o', 'l', 'd', 'e', 'r', '_', 'n', 'a', 'm', 'e', '/',
// File name
// End of Central Directory Record
'P', 'K', 5, 6, // Local File Header Signature
0, 0, // Number of this disk
0, 0, // Disk where CD starts
1, 0, // Number of CD records on this disk
1, 0, // Total number of records
58, 0, 0, 0, // Size of CD
42, 0, 0, 0, // Offset of start of CD
0, 0, // Comment length
};
try ( FileOutputStream fos = new FileOutputStream( file ) )
{
fos.write(contents);
}
try ( ZipInputStream zis = new ZipInputStream( new FileInputStream( file ) ) )
{
ZipEntry entry = zis.getNextEntry();
System.out.println( entry.getName() );
}
}
}
来源:https://stackoverflow.com/questions/33347948/coldfusion-9-cfzip-unzip-error-on-a-zip-file-created-from-coldfusion-11-cfzip