/*
* Copyright (c) 1998-2010 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Charles Reich
*/
package com.caucho.quercus.lib.zlib;
import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.annotation.NotNull;
import com.caucho.quercus.annotation.Optional;
import com.caucho.quercus.annotation.ReturnNullAsFalse;
import com.caucho.quercus.env.*;
import com.caucho.quercus.lib.file.BinaryInput;
import com.caucho.quercus.lib.file.BinaryOutput;
import com.caucho.quercus.lib.file.BinaryStream;
import com.caucho.quercus.lib.file.FileModule;
import com.caucho.quercus.lib.OutputModule;
import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.util.L10N;
import com.caucho.vfs.StreamImplOutputStream;
import com.caucho.vfs.TempBuffer;
import com.caucho.vfs.TempStream;
import com.caucho.vfs.WriteStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.Adler32;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* PHP Zlib
*/
public class ZlibModule extends AbstractQuercusModule {
private static final Logger log
= Logger.getLogger(ZlibModule.class.getName());
private static final L10N L = new L10N(ZlibModule.class);
public static final int FORCE_GZIP = 0x1;
public static final int FORCE_DEFLATE = 0x2;
private int _dbg;
public String []getLoadedExtensions()
{
return new String[] { "zlib" };
}
/**
*
* @param env
* @param fileName
* @param mode
* @param useIncludePath always on
* @return Zlib
*/
@ReturnNullAsFalse
public static BinaryStream gzopen(Env env,
StringValue fileName,
String mode,
@Optional("false") boolean useIncludePath)
{
String filemode = getFileMode(mode);
int compressionLevel = getCompressionLevel(mode);
int compressionStrategy = getCompressionStrategy(mode);
Object val = FileModule.fopen(env, fileName, mode, useIncludePath, null);
if (val == null)
return null;
try {
int ch = filemode.charAt(0);
if (ch == 'r') {
BinaryInput is = (BinaryInput) val;
return new ZlibInputStream(env, is);
}
else if (ch == 'w') {
return new ZlibOutputStream(((BinaryOutput) val).getOutputStream(),
compressionLevel,
compressionStrategy);
}
else if (ch == 'a') {
return new ZlibOutputStream(((BinaryOutput) val).getOutputStream(),
compressionLevel,
compressionStrategy);
}
else if (ch == 'x') {
return new ZlibOutputStream(((BinaryOutput) val).getOutputStream(),
compressionLevel,
compressionStrategy);
}
}
catch (IOException e) {
log.log(Level.FINE, e.getMessage(), e);
env.warning(L.l(e.getMessage()));
}
return null;
}
/**
*
* @param env
* @param fileName
* @param useIncludePath
* @return array of uncompressed lines from fileName
*/
@ReturnNullAsFalse
public static ArrayValue gzfile(Env env,
StringValue fileName,
@Optional boolean useIncludePath)
{
BinaryInput is = (BinaryInput) gzopen(env, fileName, "r", useIncludePath);
if (is == null)
return null;
try {
ArrayValue result = new ArrayValueImpl();
StringValue line;
while ((line = is.readLine(Integer.MAX_VALUE)) != null &&
line.length() > 0)
result.put(line);
return result;
} catch (IOException e) {
throw new QuercusModuleException(e);
} finally {
is.close();
}
}
public static Value ob_gzhandler(Env env, StringValue buffer, int state)
{
return OutputModule.ob_gzhandler(env, buffer, state);
}
/**
* outputs uncompressed bytes directly to browser, writes a warning message
* if an error has occured
* Note: PHP5 is supposed to print an error message but it doesn't do it
*
* @param env
* @param fileName
* @param useIncludePath
* @return number of bytes read from file, or FALSE if an error occurred
*/
public static Value readgzfile(Env env,
StringValue fileName,
@Optional boolean useIncludePath)
{
BinaryInput is = (BinaryInput) gzopen(env, fileName, "r", useIncludePath);
if (is == null)
return BooleanValue.FALSE;
try {
return LongValue.create(env.getOut().writeStream(is.getInputStream()));
} catch (IOException e) {
throw new QuercusModuleException(e);
} finally {
is.close();
}
}
/**
* Writes a string to the gzip stream.
*/
public static int gzwrite(@NotNull BinaryOutput os,
InputStream is,
@Optional("0x7fffffff") int length)
{
if (os == null)
return 0;
try {
return os.write(is, length);
} catch (IOException e) {
throw new QuercusModuleException(e);
}
}
/**
*
* @param env
* @param zp
* @param s
* @param length
* @return alias of gzwrite
*/
public int gzputs(Env env,
@NotNull BinaryOutput os,
InputStream is,
@Optional("0x7ffffff") int length)
{
if (os == null)
return 0;
try {
return os.write(is, length);
} catch (IOException e) {
throw new QuercusModuleException(e);
}
}
/**
* Closes the stream.
*/
public boolean gzclose(@NotNull BinaryStream os)
{
if (os == null)
return false;
os.close();
return true;
}
/**
* Returns true if the GZip stream is ended.
*/
public boolean gzeof(@NotNull BinaryStream binaryStream)
{
if (binaryStream == null)
return true;
return binaryStream.isEOF();
}
/**
* Reads a character from the stream.
*/
public static Value gzgetc(Env env, @NotNull BinaryInput is)
{
if (is == null)
return BooleanValue.FALSE;
try {
int ch = is.read();
if (ch < 0)
return BooleanValue.FALSE;
else {
StringValue sb = env.createBinaryBuilder(1);
sb.appendByte(ch);
return sb;
}
} catch (IOException e) {
throw new QuercusModuleException(e);
}
}
/**
* Reads a chunk of data from the gzip stream.
*/
public Value gzread(@NotNull BinaryInput is, int length)
{
if (is == null)
return BooleanValue.FALSE;
try {
return is.read(length);
} catch (IOException e) {
throw new QuercusModuleException(e);
}
}
/**
* Reads a line from the input stream.
*/
public static Value gzgets(Env env,
@NotNull BinaryInput is,
int length)
{
return FileModule.fgets(env, is, length);
}
/**
* Reads a line from the zip stream, stripping tags.
*/
public static Value gzgetss(Env env,
@NotNull BinaryInput is,
int length,
@Optional Value allowedTags)
{
return FileModule.fgetss(env, is, length, allowedTags);
}
/**
* Rewinds the stream to the very beginning
*/
public boolean gzrewind(@NotNull BinaryStream binaryStream)
{
if (binaryStream == null)
return false;
return binaryStream.setPosition(0);
}
/**
* Set stream position to the offset
* @param offset absolute position to set stream to
* @param whence if set, changes the interpretation of offset like fseek
* @return 0 upon success, else -1 for error
*/
public int gzseek(@NotNull BinaryStream binaryStream,
long offset,
@Optional("FileModule.SEEK_SET") int whence)
{
if (binaryStream == null)
return -1;
if (binaryStream.seek(offset, whence) == -1)
return -1;
return 0;
}
/**
* Gets the current position in the stream
* @return the position in the stream, or FALSE for error
*/
public Value gztell(@NotNull BinaryStream binaryStream)
{
if (binaryStream == null)
return BooleanValue.FALSE;
return LongValue.create(binaryStream.getPosition());
}
/**
* Prints out the remaining data in the stream to stdout
*/
public Value gzpassthru(Env env, @NotNull BinaryInput is)
{
WriteStream out = env.getOut();
TempBuffer tempBuf = TempBuffer.allocate();
byte[] buffer = tempBuf.getBuffer();
int length = 0;
try {
int sublen = is.read(buffer, 0, buffer.length);
while (sublen > 0) {
out.write(buffer, 0, sublen);
length += sublen;
sublen = is.read(buffer, 0, buffer.length);
}
return LongValue.create(length);
} catch (IOException e) {
log.log(Level.FINE, e.toString(), e);
return BooleanValue.FALSE;
} finally {
TempBuffer.free(tempBuf);
}
}
/**
* Returns the encoding type both allowed by the server
* and supported by the user's browser.
*/
public Value zlib_get_coding_type(Env env)
{
String ini = env.getIniString("zlib.output_compression");
if (ini == null || ini == "")
return BooleanValue.FALSE;
//zlib_get_coding_type can also be an integer > 0
if (! ini.equalsIgnoreCase("on")) {
int ch = ini.charAt(0);
if (ch < '0' || ch > '9')
return BooleanValue.FALSE;
}
ServerArrayValue sav = new ServerArrayValue(env);
Value val = sav.get(env.createString("HTTP_ACCEPT_ENCODING"));
if (!val.isset())
return BooleanValue.FALSE;
String s = val.toString();
if (s.contains("gzip"))
return env.createString("gzip");
else if(s.contains("deflate"))
return env.createString("deflate");
else
return BooleanValue.FALSE;
}
/**
* compresses data using zlib
*
* @param data
* @param level (default is Deflater.DEFAULT_COMPRESSION)
* @return compressed string
*/
public Value gzcompress(Env env,
InputStream data,
@Optional("6") int level)
{
TempBuffer tempBuf = TempBuffer.allocate();
byte []buffer = tempBuf.getBuffer();
Deflater deflater = null;
try {
deflater = new Deflater(level, true);
Adler32 crc = new Adler32();
boolean isFinished = false;
StringValue out = env.createLargeBinaryBuilder();
buffer[0] = (byte) 0x78;
if (level <= 1)
buffer[1] = (byte) 0x01;
else if (level < 6)
buffer[1] = (byte) 0x5e;
else if (level == 6)
buffer[1] = (byte) 0x9c;
else
buffer[1] = (byte) 0xda;
out.append(buffer, 0, 2);
int len;
while (! isFinished) {
while (! isFinished && deflater.needsInput()) {
len = data.read(buffer, 0, buffer.length);
if (len > 0) {
crc.update(buffer, 0, len);
deflater.setInput(buffer, 0, len);
}
else {
isFinished = true;
deflater.finish();
}
}
while ((len = deflater.deflate(buffer, 0, buffer.length)) > 0) {
out.append(buffer, 0, len);
}
}
long value = crc.getValue();
buffer[0] = (byte) (value >> 24);
buffer[1] = (byte) (value >> 16);
buffer[2] = (byte) (value >> 8);
buffer[3] = (byte) (value >> 0);
out.append(buffer, 0, 4);
return out;
} catch (Exception e) {
throw QuercusModuleException.create(e);
} finally {
TempBuffer.free(tempBuf);
if (deflater != null)
deflater.end();
}
}
/**
*
* @param data
* @param length (maximum length of string returned)
* @return uncompressed string
*/
public Value gzuncompress(Env env,
InputStream is,
@Optional("0") long length)
{
TempBuffer tempBuf = TempBuffer.allocate();
byte []buffer = tempBuf.getBuffer();
InflaterInputStream in = null;
try {
if (length == 0)
length = Long.MAX_VALUE;
in = new InflaterInputStream(is);
StringValue sb = env.createLargeBinaryBuilder();
int len;
while ((len = in.read(buffer, 0, buffer.length)) >= 0) {
sb.append(buffer, 0, len);
}
return sb;
} catch (Exception e) {
throw QuercusModuleException.create(e);
} finally {
TempBuffer.free(tempBuf);
try {
if (in != null)
in.close();
} catch (Exception e) {
}
}
}
/**
*
* @param level
* @return compressed using DEFLATE algorithm
*/
public Value gzdeflate(Env env, InputStream data,
@Optional("6") int level)
{
TempBuffer tempBuf = TempBuffer.allocate();
byte []buffer = tempBuf.getBuffer();
Deflater deflater = null;
try {
deflater = new Deflater(level, true);
boolean isFinished = false;
TempStream out = new TempStream();
int len;
while (! isFinished) {
if (! isFinished && deflater.needsInput()) {
len = data.read(buffer, 0, buffer.length);
if (len > 0)
deflater.setInput(buffer, 0, len);
else {
isFinished = true;
deflater.finish();
}
}
while ((len = deflater.deflate(buffer, 0, buffer.length)) > 0) {
out.write(buffer, 0, len, false);
}
}
deflater.end();
return env.createBinaryString(out.getHead());
} catch (Exception e) {
throw QuercusModuleException.create(e);
} finally {
TempBuffer.free(tempBuf);
if (deflater != null)
deflater.end();
}
}
/**
* @param data compressed using Deflate algorithm
* @param length of data to decompress
*
* @return uncompressed string
*/
public Value gzinflate(Env env,
InputStream data,
@Optional("0") int length)
{
if (length <= 0)
length = Integer.MAX_VALUE;
TempBuffer tempBuf = TempBuffer.allocate();
byte []buffer = tempBuf.getBuffer();
Inflater inflater = null;
try {
inflater = new Inflater(true);
StringValue sb = env.createBinaryBuilder();
while (true) {
int sublen = Math.min(length, buffer.length);
sublen = data.read(buffer, 0, sublen);
if (sublen > 0) {
inflater.setInput(buffer, 0, sublen);
length -= sublen;
int inflatedLength;
while ((inflatedLength = inflater.inflate(buffer, 0, sublen)) > 0) {
sb.append(buffer, 0, inflatedLength);
}
}
else
break;
}
return sb;
} catch (OutOfMemoryError e) {
env.warning(e);
return BooleanValue.FALSE;
} catch (Exception e) {
env.warning(e);
return BooleanValue.FALSE;
} finally {
TempBuffer.free(tempBuf);
if (inflater != null)
inflater.end();
}
}
/**
*
* Compresses data using the Deflate algorithm, output is
* compatible with gzwrite's output
*
* @param data compressed with the Deflate algorithm
* @param level Deflate compresion level [0-9]
* @param encodingMode CRC32 trailer is not written if encoding mode
* is FORCE_DEFLATE, default is to write CRC32
* @return StringValue with gzip header and trailer
*/
public Value gzencode(Env env, InputStream is,
@Optional("6") int level,
@Optional("1") int encodingMode)
{
TempBuffer tempBuf = TempBuffer.allocate();
byte[] buffer = tempBuf.getBuffer();
TempStream ts = new TempStream();
StreamImplOutputStream out = new StreamImplOutputStream(ts);
ZlibOutputStream gzOut = null;
try {
gzOut = new ZlibOutputStream(out, level,
Deflater.DEFAULT_STRATEGY,
encodingMode);
int len;
while ((len = is.read(buffer, 0, buffer.length)) > 0) {
gzOut.write(buffer, 0, len);
}
gzOut.close();
StringValue sb = env.createBinaryBuilder();
for (TempBuffer ptr = ts.getHead(); ptr != null; ptr = ptr.getNext())
sb.append(ptr.getBuffer(), 0, ptr.getLength());
return sb;
} catch(IOException e) {
throw QuercusModuleException.create(e);
} finally {
TempBuffer.free(tempBuf);
ts.destroy();
if (gzOut != null)
gzOut.close();
}
}
/**
* Helper function to retrieve the filemode closest to the end
* Note: PHP5 unexpectedly fails when 'x' is the mode.
*
* XXX todo: toss a warning if '+' is found
* (gzip cannot be open for both reading and writing at the same time)
*
*/
private static String getFileMode(String input)
{
String modifier = "";
String filemode = input.substring(0, 1);
for (int i = 1; i < input.length(); i++ )
{
char ch = input.charAt(i);
switch (ch) {
case 'r':
filemode = "r";
break;
case 'w':
filemode = "w";
break;
case 'a':
filemode = "a";
break;
case 'b':
modifier = "b";
break;
case 't':
modifier = "t";
break;
}
}
return filemode + modifier;
}
/**
* Helper function to retrieve the compression level
* - finds the compression level nearest to the end and returns that
*/
private static int getCompressionLevel(String input)
{
for (int i = input.length() - 1; i >= 0; i--) {
char ch = input.charAt(i);
if (ch >= '0' && ch <= '9')
return ch - '0';
}
return Deflater.DEFAULT_COMPRESSION;
}
/**
* Helper function to retrieve the compression strategy.
* - finds the compression strategy nearest to the end and returns that
*/
private static int getCompressionStrategy(String input)
{
for (int i = input.length() - 1; i >= 0; i--) {
char ch = input.charAt(i);
switch (ch) {
case 'f':
return Deflater.FILTERED;
case 'h':
return Deflater.HUFFMAN_ONLY;
}
}
return Deflater.DEFAULT_STRATEGY;
}
}