[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 use cfexecute
to run 7zip external to ColdFusion or use the java org.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 in CFZip
); 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. use cfexecute
to call an external program to handle the zip files; use the java org.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() );
}
}
}