diff --git a/extramodules/it/eng/jhove/module/odf/JingDriver.java b/extramodules/it/eng/jhove/module/odf/JingDriver.java new file mode 100644 index 000000000..dc3835fbb --- /dev/null +++ b/extramodules/it/eng/jhove/module/odf/JingDriver.java @@ -0,0 +1,76 @@ +package it.eng.jhove.module.odf; + +import com.thaiopensource.util.PropertyMapBuilder; +import com.thaiopensource.validate.SchemaReader; +import com.thaiopensource.validate.ValidateProperty; +import com.thaiopensource.validate.ValidationDriver; +import com.thaiopensource.validate.rng.RngProperty; +import com.thaiopensource.xml.sax.ErrorHandlerImpl; +import java.io.IOException; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import java.io.OutputStream; + +import java.io.InputStream; + + +/** + * JingDriver is a wrapper to use Jing as a library and not as an + * application without changing jing own source code. + * + * Created: Fri Oct 20 14:22:14 2006 + * + * @author Gian Uberto Lauri + * @version $Revision$ + */ +public class JingDriver { + + /** + * doValidation preform a relaxed ng validation using jing + * code (-i option, since we had some barfing with Oasis nrg during + * pre-tests) + * + * @param schema a String relaxed nrg schema against who + * the file is to be validated. + * @param xmlFileName a String xml file to validate + * @param boust a ByteArrayOutputStream byte output stream + * to collect jing barfing... + * @return a boolean true if the file is valid, false + * otherwise. + */ + public boolean doValidation(InputStream schema, + String xmlFileName, + OutputStream boust) { + ErrorHandlerImpl eh = new ErrorHandlerImpl(System.out); + PropertyMapBuilder properties = new PropertyMapBuilder(); + ValidateProperty.ERROR_HANDLER.put(properties, eh); + RngProperty.CHECK_ID_IDREF.add(properties); + SchemaReader sr = null; + + // Simulate -i option, since without the program barfs on + // Oasis Open Document nrg. + properties.put(RngProperty.CHECK_ID_IDREF, null); + + boolean hadError = false; + try { + ValidationDriver driver = new ValidationDriver(properties.toPropertyMap(), sr); + InputSource in = new InputSource(schema); + if (driver.loadSchema(in)) { + + if (!driver.validate(ValidationDriver.uriOrFileInputSource(xmlFileName))) + hadError = true; + } + else + hadError = true; + } + catch (SAXException e) { + hadError = true; + eh.printException(e); + } + catch (IOException e) { + hadError = true; + eh.printException(e); + } + return ! hadError; + } +} diff --git a/extramodules/it/eng/jhove/module/odf/ManifestHandler.java b/extramodules/it/eng/jhove/module/odf/ManifestHandler.java new file mode 100644 index 000000000..7d98cb817 --- /dev/null +++ b/extramodules/it/eng/jhove/module/odf/ManifestHandler.java @@ -0,0 +1,82 @@ +package it.eng.jhove.module.odf; + +import org.xml.sax.helpers.DefaultHandler; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import java.util.List; +import it.eng.jhove.Booolean; + +/** + * Describe class ManifestHandler here. + * + * + * Created: Mon Oct 23 08:45:53 2006 + * + * @author Gian Uberto Lauri + * @version $Revision$ + */ +public class ManifestHandler extends DefaultHandler { + + private Booolean isEncrypted; + private List entries; + + /** + * Crea una nuova istanza di ManifestHandler . + * + */ + public ManifestHandler(List entries, + Booolean isEncrypted) { + this.entries = entries; + this.isEncrypted = isEncrypted; + } + + /** + * startElement + * + * @param nameSpaceUri a String + * @param localName a String + * @param rawName a String + * @param attributes an Attributes + * @exception SAXException + */ + public final void startElement(final String nameSpaceUri, + final String localName, + final String rawName, + final Attributes attributes) + throws SAXException { + + if (rawName.equals(FILE_ENTRY)) { + String type=""; + String path=""; + int size=0; + + for (int i = 0; i < attributes.getLength(); i++) { + if (attributes.getQName(i).equals(ATTR_MEDIA)) { + type = attributes.getValue(i); + } + if (attributes.getQName(i).equals(ATTR_PATH)) { + path = attributes.getValue(i); + } + if (attributes.getQName(i).equals(ATTR_SIZE)) { + size = Integer.parseInt(attributes.getValue(i)); + } + + + } + ManifestEntry entry = new ManifestEntry(type, path, size); + entries.add(entry); + } + else if (rawName.equals(CRYP_ENTRT)) { + isEncrypted.setFlag(true); + } + + } + + private final static String FILE_ENTRY = "manifest:file-entry"; + + private final static String CRYP_ENTRT = "manifest:encryption-data"; + + private final static String ATTR_MEDIA = "manifest:media-type"; + private final static String ATTR_PATH = "manifest:full-path"; + private final static String ATTR_SIZE = "manifest:size"; +} diff --git a/extramodules/it/eng/jhove/module/odf/MetaHandler.java b/extramodules/it/eng/jhove/module/odf/MetaHandler.java new file mode 100644 index 000000000..9c77566d2 --- /dev/null +++ b/extramodules/it/eng/jhove/module/odf/MetaHandler.java @@ -0,0 +1,87 @@ +package it.eng.jhove.module.odf; + +import org.xml.sax.helpers.DefaultHandler; +import edu.harvard.hul.ois.jhove.RepInfo; +import org.xml.sax.SAXException; +import org.xml.sax.Attributes; +import java.text.SimpleDateFormat; +import java.text.ParsePosition; +import java.util.Date; + +/** + * Describe class MetaHandler here. + * + * + * Created: Mon Oct 23 13:25:46 2006 + * + * @author Gian Uberto Lauri + * @version $Revision$ + */ +public class MetaHandler extends DefaultHandler { + protected RepInfo info; + protected StringBuffer tagContent; + protected boolean isInDate=false; + protected boolean isInCreationDate=false; + protected SimpleDateFormat sdf1=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + /** + * Cretes a new instance of MetaHandler . + * + */ + public MetaHandler(RepInfo info) { + this.info= info; + } + + /** + * SAX parser callback method. + */ + public void startElement (String namespaceURI, String localName, + String rawName, Attributes atts) + throws SAXException + { + tagContent = new StringBuffer (); + if (rawName.equals(TAG_DATE)) { + isInDate=true; + } + if (rawName.equals(TAG_CREATION)) { + isInCreationDate=true; + } + + } + + /** + * SAX parser callback method. + */ + public void characters (char [] ch, int start, int length) + throws SAXException + { + tagContent.append (ch, start, length); + + } + + /** + * SAX parser callback method. + */ + public void endElement (String namespaceURI, String localName, + String rawName) + throws SAXException + { + if ( isInDate ) { + info.setLastModified(contentToDate()); + isInDate=false; + } + if ( isInCreationDate ) { + info.setCreated(contentToDate()); + isInCreationDate=false; + } + + + } + + private final Date contentToDate() { + // Open document date format is yyyy-mm-ddThh:mm:ss + return sdf1.parse(tagContent.toString(), new ParsePosition(0)); + + } + private final static String TAG_DATE="dc:date"; + private final static String TAG_CREATION="meta:creation-date"; +} diff --git a/extramodules/it/eng/jhove/module/odf/OdfModule.java b/extramodules/it/eng/jhove/module/odf/OdfModule.java new file mode 100644 index 000000000..84d84c959 --- /dev/null +++ b/extramodules/it/eng/jhove/module/odf/OdfModule.java @@ -0,0 +1,775 @@ +package it.eng.jhove.module.odf; + +import edu.harvard.hul.ois.jhove.ErrorMessage; +import edu.harvard.hul.ois.jhove.InfoMessage; +import edu.harvard.hul.ois.jhove.JhoveBase; +import edu.harvard.hul.ois.jhove.JhoveException; +import edu.harvard.hul.ois.jhove.Module; +import edu.harvard.hul.ois.jhove.ModuleBase; +import edu.harvard.hul.ois.jhove.OutputHandler; +import it.eng.jhove.Booolean; +import it.eng.jhove.RepInfo; +import it.eng.jhove.NullHandler; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + + +/** + * Class OdfModule A module for Jhove - JSTOR/Harvard Object + * Validation Environment intended to recognize and validate ODF files + * formatted according to the definition in "Open Document Format for + * Office Applications (OpenDocument) v1.0" + * + * + * Created: Fri Oct 20 15:59:25 2006 + * + * @author Gian Uberto Lauri + * @version $Revision$ + */ +public class OdfModule extends ModuleBase implements Module { + + + /** + * Crea una nuova istanza di OdfModule . + * + */ + public OdfModule() { + super (NAME, RELEASE, DATE, FORMAT, COVERAGE, MIMETYPES, WELLFORMED, + VALIDITY, REPINFO, NOTE, RIGHTS, RANDOM); + + } + + /** + * init + * + * @param string a String + * @exception Exception + */ + public final void init(final String string) throws Exception { + + } + + /** + * initParse initializes the status of the parser. + * + */ + public final void initParse() { + super.initParse(); + } + + /** + * parse + * + * @param inputStream an InputStream + * @param repInfo a RepInfo + * @param n an int + * @return an int + * @exception IOException + */ + public final int parse(final InputStream inputStream, + final RepInfo repInfo, + final int n) throws IOException { + boolean alreadyNonPreservable=false; + + initParse(); + repInfo.setFormat(FORMAT[0]); + repInfo.setModule (this); + + // First step. To work with ZIP classes you need a File, not a + // stream. But you have a stream and this stream could arrive + // from far far away... Solution, build yourself a nice + // temporary file. Well'use quite some within this module... + + String tempdir = _je.getTempDirectory(); + File tempFile = null; + if (tempdir == null) { + tempFile = File.createTempFile("odfTemp", ".zip"); + + } + else { + tempFile = File.createTempFile("odfTemp", ".zip", + new File(tempdir)); + + } + + + tempFile.deleteOnExit(); + + FileOutputStream fous = new FileOutputStream(tempFile); + + byte b[] = new byte[1]; + + while (inputStream.read(b) == 1) { + fous.write(b); + } + + fous.close(); + + // ------------ Now let's begin + ZipFile zipf=null; +// Map componentFiles = new HashMap(); + + FileInputStream fins = new FileInputStream(tempFile); + + boolean matchesP = doTheCheck((InputStream)fins); + + fins.close(); + + if (!matchesP) { + repInfo.setWellFormed(false); + return(0); + } + + try { + + zipf = new ZipFile(tempFile); + + ZipEntry mimeTypeZE = zipf.getEntry(MIMETYPE); + + if (mimeTypeZE == null) { + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage("Corrupted or invaild ODF Package: mimetype comonent is missing.")); + zipf.close(); + return 0; + + } + + ZipEntry manifestZE = zipf.getEntry(MANIFEST); + if (manifestZE == null) { + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage("Corrupted or invaild ODF Package: manifest is missing.")); + zipf.close(); + return 0; + + } + + if (! setProfileMime(repInfo, zipf, mimeTypeZE)) { + repInfo.setValid(false); + repInfo.setWellFormed(true); + repInfo.setMessage(new ErrorMessage("Invalid ODF Package, unexpected type: " + repInfo.getMimeType())); + zipf.close(); + return 0; + + } + repInfo.setSigMatch(_name); + + // Process Manifest + String manifestFileName = dumpPart(manifestZE, zipf); + JingDriver jd = new JingDriver(); + + ByteArrayOutputStream errorStream = new ByteArrayOutputStream(1024); + + if (! jd.doValidation(getResource(SCHEMA_MANIFEST), + manifestFileName, + errorStream) ) { + StringBuffer buf = new StringBuffer("Invalid ODF Package, manifest failed Relaxed NG validation: " ); + buf.append(errorStream.toString()); + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage(buf.toString())); + } + + XMLReader parser = getXmlParser(); + List elementList = new ArrayList(); + Booolean isEncrypted = new Booolean(false,""); + ManifestHandler manifestHandler = new ManifestHandler(elementList, + isEncrypted); + + parser.setContentHandler (manifestHandler); + + /* + * Attempt to set schema awareness to avoid validation + * errors. + */ + try { + parser.setFeature("http://xml.org/sax/features/validation", + false); + parser.setFeature("http://xml.org/sax/features/namespace-prefixes", + true); + parser.setProperty ("http://java.sun.com/xml/jaxp/" + + "properties/schemaLanguage", + "http://www.w3.org/2001/XMLSchema"); + } + catch (SAXException e) + {} + + try { + parser.parse (manifestFileName); + + } catch (SAXException excptn) { + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage("Mmalformed manifest : " + + excptn.getMessage())); + zipf.close(); + return 0; + + } + // Process components + JhoveBase jhoveBase; + try { + + jhoveBase = new JhoveBase(); + jhoveBase.init(_je.getConfigFile(), + _je.getSaxClass()); + + } catch (JhoveException excptn) { + + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage("Can't instance engine to analyze package parts. Cause : " + + excptn.getMessage())); + zipf.close(); + return 0; + } + + for (Iterator iter = elementList.iterator(); iter.hasNext();) { + ManifestEntry mnfe = (ManifestEntry) iter.next(); + + /* + * If the size field is set then the data is + * encrypted, therefore we can't process it. If the + * type equals application/binary (not a IANA + * registered one!) then the data is skipped as + * application specific. + */ + if (mnfe.size != 0) { + StringBuffer tmp = new StringBuffer("Warning, part "); + tmp.append(mnfe.fullPath); + tmp.append(" is encrypted, validation skipped."); + repInfo.setMessage(new InfoMessage(tmp.toString())); + } + else if (mimeTypeMap.get(mnfe.mediaType) != null && + ! mnfe.fullPath.equals(SKIP_ROOT)) { + String subDocName; + int idxof = mnfe.fullPath.indexOf("/"); + if (idxof<1) { + + subDocName= mnfe.fullPath.substring(0, idxof); + + } + else { + + subDocName = mnfe.fullPath; + + } + + RepInfo subDoc = new RepInfo(subDocName); + subDoc.setModule(this); + subDoc.setFormat("ODF"); + subDoc.setProfile((String)mimeTypeMap.get(mnfe.mediaType)); + subDoc.setMimeType(mnfe.mediaType); + subDoc.setValid(true); + subDoc.setWellFormed(true); + subDoc.setConsistent(true); + repInfo.putEmbeddedRepInfo(subDocName, subDoc); + } + else if ( mnfe.mediaType.equals("") || + mnfe.mediaType.equals(SKIP_TYPE) || + mnfe.mediaType.startsWith(SKIP_APP)) { + + } + else { + ZipEntry part = zipf.getEntry(mnfe.fullPath); + if (part==null) { + // Manifest holds information of "virtual" + // file entries, like Configurations2 (a directory) + break; + } + + // good for processing + if (! part.isDirectory() && + part.getSize() > 0) { + String partFileName = dumpPart(part, zipf); + + if (mnfe.mediaType.equals(XRNG_TYPE)) { + if (! jd.doValidation(getResource(SCHEMA_OPENDOCUMENT), + partFileName, + errorStream)) { + + NullHandler nullHandler = new NullHandler(); + + try { + jhoveBase.dispatch(_app, + null, + null, + nullHandler, + null, + new String[] {partFileName}); + + String partDocName; + int idxofp = mnfe.fullPath.indexOf("/"); + if (idxofp<1) { + + partDocName = mnfe.fullPath; + + } + else { + + partDocName= mnfe.fullPath.substring(0, idxofp-1); + + } + + RepInfo current = repInfo.getEmbeddedRepInfo(partDocName); + if (current==null) { + current=repInfo; + } + + + for (Iterator iter2 = nullHandler.getRepInfos(); iter2.hasNext();) { + if ( ((RepInfo)iter2.next()).getWellFormed() != RepInfo.TRUE) { + repInfo.setValid(false); + } + } + + if (repInfo.getValid() == RepInfo.TRUE && ! alreadyNonPreservable) { + alreadyNonPreservable=true; + repInfo.setProfile((String)mimeTypeMap.get(repInfo.getMimeType())); + } + else { + StringBuffer buf = new StringBuffer("Invalid ODF Package, component "); + buf.append(mnfe.fullPath); + buf.append(" failed both Relaxed NG and normal XML validation therefore" ); + buf.append(" is not well formed."); + repInfo.setValid(false); + repInfo.setMessage(new ErrorMessage(buf.toString())); + } + + + + } catch (Exception excptn) { + StringBuffer buf = new StringBuffer("Invalid ODF Package, component "); + buf.append(mnfe.fullPath); + buf.append(" failed both Relaxed NG and normal XML validation " ); + buf.append(" due this error: "); + buf.append(excptn.getMessage()); + repInfo.setValid(false); + repInfo.setMessage(new ErrorMessage(buf.toString())); + } + } + if (mnfe.fullPath.equals(META_FILE)) { + MetaHandler metaHandler = new MetaHandler(repInfo); + + parser.setContentHandler (metaHandler); + + try { + parser.parse (partFileName); + + } catch (SAXException excptn) { + repInfo.setValid(false); + repInfo.setMessage(new ErrorMessage("malformed meta.xml: " + + excptn.getMessage())); + } + + } + } + else if (mnfe.mediaType.startsWith(IMG_TYPE)) { + // For the files in the Picture directory + NullHandler nullHandler = new NullHandler(); + + try { + jhoveBase.dispatch(_app, + null, + null, + nullHandler, + null, + new String[] {partFileName}); + + } catch (Exception excptn) { + StringBuffer xxx = new StringBuffer("File "); + xxx.append(mnfe.fullPath); + xxx.append("analysis failed. Cause: "); + xxx.append(excptn.getMessage()); + repInfo.setMessage(new ErrorMessage(xxx.toString())); + } + + + String partDocName; + int idxofp = mnfe.fullPath.indexOf("/"); + if (idxofp<10) { + + partDocName = mnfe.fullPath; + + } + else { + + partDocName= mnfe.fullPath.substring(0, idxofp-1); + + } + + RepInfo current = repInfo.getEmbeddedRepInfo(partDocName); + if (current==null) { + current=repInfo; + } + + + for (Iterator iter2 = nullHandler.getRepInfos(); iter2.hasNext();) { + current.addContainedRepInfo((RepInfo)iter2.next()); + } + + } + + + + } // End "good for processing" + + } + + } // end of the loop on the manifest. + + + } catch (ZipException excptn) { + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage("Invalid or corrupted ODF Package: " + + excptn.getMessage())); + } catch (IOException excptn) { + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage("Error during ODF analysis: " + + excptn.getMessage())); + } catch (IllegalStateException excptn) { + repInfo.setValid(false); + repInfo.setWellFormed(false); + repInfo.setMessage(new ErrorMessage("Unexpected end of stream: " + + excptn.getMessage())); + + } + catch (Throwable excptn) { + // TODO remove after debugging + excptn.printStackTrace(); + } + finally { + try { + zipf.close(); + } catch (Throwable excptn) { + + } + + } + + // ------------ no other passes needed. + return 0; + } + + /** + * parse + * + * @param randomAccessFile a RandomAccessFile + * @param repInfo a RepInfo + * @exception IOException + */ + public final void parse(final RandomAccessFile randomAccessFile, + final RepInfo repInfo) throws IOException { + // Not used + } + + /** + * param + * + * @param string a String + * @exception Exception + */ + public final void param(final String string) throws Exception { + + } + + /** + * getDefaultParams + * + * @return a List + */ + public final List getDefaultParams() { + return null; + } + + /** + * checkSignatures Checs for a valid Open Document Format file: + * the string "PK" at position 0, the string "mimetype" at position 30. + * + * @param file a File + * @param inputStream an InputStream + * @param repInfo a RepInfo + * @exception IOException + */ + public final void checkSignatures(final File file, + final InputStream inputStream, + final RepInfo repInfo) throws IOException { + + // If we got this far, take note that the signature is OK. + repInfo.setConsistent (doTheCheck(inputStream)); + + + } + + private final boolean doTheCheck(final InputStream inputStream) + throws IOException{ + DataInputStream dis = getBufferedDataStream (inputStream, (_app != null) ? + _je.getBufferSize () : 0); + boolean rv = true; + for (int i = 0; i < 38; i++) { + int c = readUnsignedByte(dis,this); + rv &= ( header[i] == ' ' || header[i] == c ); + } + return rv; + } + + /** + * checkSignatures Not used + * + * @param file a File + * @param randomAccessFile a RandomAccessFile + * @param repInfo a RepInfo + * @exception IOException + */ + public final void checkSignatures(final File file, + final RandomAccessFile randomAccessFile, + final RepInfo repInfo) + throws IOException { + } + + /** + * show + * + * @param outputHandler an OutputHandler + */ + public final void show(final OutputHandler outputHandler) { + + } + + + /** + * setVerbosity + * + * @param n an int + */ + public final void setVerbosity(final int n) { + + } + + + private final boolean setProfileMime(RepInfo repInfo, ZipFile zipf, ZipEntry zipnt) + throws IOException, ZipException, IllegalStateException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + InputStream inst = new BufferedInputStream(zipf.getInputStream(zipnt)); + + byte b[] = new byte[1]; + + while (inst.read(b,0,1) != -1) { + buf.write(b); + } + inst.close(); + + repInfo.setMimeType(buf.toString()); + + repInfo.setProfile((String)mimeTypeMap.get(repInfo.getMimeType())); + + return (repInfo.getProfile() != null); + } + + + private final String dumpPart(ZipEntry zent, ZipFile zfle) + throws IOException, ZipException{ + String name = zent.getName().replace('/', '_'); + + File fout = File.createTempFile(name, ".odfpart"); + String returnName = fout.getAbsolutePath(); + fout.deleteOnExit(); + + OutputStream fous = new BufferedOutputStream( new FileOutputStream(fout)); + InputStream inst = new BufferedInputStream(zfle.getInputStream(zent)); + + byte b[] = new byte[1]; + + while (inst.read(b) != -1) { + fous.write(b); + } + inst.close(); + fous.close(); + return returnName; + } + + private final InputStream getResource(String resourceName) { + StringBuffer buf = new StringBuffer(RESOURCES); + buf.append(resourceName); + return getClass().getResourceAsStream(buf.toString()); + } + + private XMLReader getXmlParser() + throws IOException { + String saxClass = JhoveBase.getSaxClassFromProperties (); + XMLReader parser = null; + try { + if (saxClass == null) { + /* Use Java 1.4 methods to create default parser. + */ + SAXParserFactory factory = + SAXParserFactory.newInstance(); + factory.setNamespaceAware (true); + parser = factory.newSAXParser ().getXMLReader (); + } + else { + parser = XMLReaderFactory.createXMLReader (saxClass); + } + } + catch (ParserConfigurationException e) { + // If we can't get a SAX parser, we're stuck. + throw new IOException ("SAX parser not found: " + + saxClass +": "+ e.getMessage()); + } + catch (SAXException excptn) { + throw new IOException ("SAX parser not found: " + + saxClass +": "+ excptn.getMessage()); + } + + return parser; + } + + + private static final String NAME = "ODF-engineering"; + private static final String RELEASE = "1.0"; + private static final int DATE[] = {2006, 9, 25}; + private static final String FORMAT[] = { + "ODF", + "Open Document Format for Office Applications 1.0" + }; + private static final String MIMETYPES[] = {"application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.text-template", + "application/vnd.oasis.opendocument.graphics", + "application/vnd.oasis.opendocument.graphics-template", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.presentation-template", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.spreadsheet-template", + "application/vnd.oasis.opendocument.chart", + "application/vnd.oasis.opendocument.chart-template", + "application/vnd.oasis.opendocument.image", + "application/vnd.oasis.opendocument.image-template", + "application/vnd.oasis.opendocument.formula", + "application/vnd.oasis.opendocument.formula-template", + "application/vnd.oasis.opendocument.text-master", + "application/vnd.oasis.opendocument.text-web"}; + + private static final String COVERAGE = "ODF"; + private static final String WELLFORMED = null; + private static final String VALIDITY = null; + private static final String REPINFO = null; + private static final String NOTE = "Work in progress"; + private static final String RIGHTS = + "Copyright 2006 Engineering Ingengeria Informatica S.p.a." + + "Released under the GNU Lesser General Public License." + + "Cryptoserver Library Copyright Engiweb Security, all rights reserved"; + private static final boolean RANDOM = false; + + private static final String PROFILES[] = {"Open Document Format Text Document", + "Open Document Format Text Document Template", + "Open Document Format Drawing", + "Open Document Format Drawing Template", + "Open Document Format Presentation Document", + "Open Document Format Presentation Document Template", + "Open Document Format Spreadsheet", + "Open Document Format Spreadsheet Template", + "Open Document Format Chart", + "Open Document Format Spreadsheet Chart Template", + "Open Document Format Image", + "Open Document Format Image Template", + "Open Document Format Mathematic Formula", + "Open Document Format Mathematic Formula Template", + "Open Document Format Global Text Document", + "Open Document Format HTML Text Document Template"}; + + private static final Map mimeTypeMap = new HashMap(); + + static { + for (int i = 0; i < MIMETYPES.length; i++) { + mimeTypeMap.put(MIMETYPES[i],PROFILES[i]); + } + } + + // Magic number + private static final int header[] =new int[]{'P', 'K', ' ', ' ', + ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', + ' ', ' ', ' ', ' ', + ' ', ' ', 'm', 'i', + 'm', 'e', 't', 'y', + 'p', 'e'}; + + // These are the name of the mandatory ZipEntries in an Open + // Document Format file. + private final static String MIMETYPE="mimetype"; + private final static String MANIFEST="META-INF/manifest.xml"; + + // Schemas for the Relax NG validation + private final static String SCHEMA_MANIFEST = "OpenDocument-manifest-schema-v1.0-os.rng"; + private final static String SCHEMA_OPENDOCUMENT = "OpenDocument-schema-v1.0-os.rng"; + private final static String RESOURCES = "resources/"; + + // Media types that need "special processing" + private final static String + SKIP_TYPE = "application/binary"; // this is a non standard media + // type that has to be skipped + + private final static String + XRNG_TYPE = "text/xml"; // all the xml file in a Open + // Document file are to be + // validated againist + // SCHEMA_OPENDOCUMENT + + private final static String + SKIP_APP = "application/"; // Application specific binary + // stuff, i.e. substitutes for + // a faster OLE display or + + private final static String + IMG_TYPE = "image/"; // Images have their own + // processing... + + // Special contents + private final static String META_FILE = "meta.xml"; + private final static String SKIP_ROOT = "/"; + +} + + +// package access structure. +final class ManifestEntry { + final String mediaType; + final String fullPath; + final int size; + + ManifestEntry(String mediaType, String fullPath, int size) { + this.mediaType = mediaType; + this.fullPath = fullPath; + this.size = size; + } + +} diff --git a/extramodules/it/eng/jhove/module/png/PngModule.java b/extramodules/it/eng/jhove/module/png/PngModule.java new file mode 100644 index 000000000..20f4d2eb5 --- /dev/null +++ b/extramodules/it/eng/jhove/module/png/PngModule.java @@ -0,0 +1,1341 @@ +package it.eng.jhove.module.png; + +import edu.harvard.hul.ois.jhove.Agent; +import edu.harvard.hul.ois.jhove.AgentType; +import edu.harvard.hul.ois.jhove.Checksummer; +import edu.harvard.hul.ois.jhove.Document; +import edu.harvard.hul.ois.jhove.DocumentType; +import edu.harvard.hul.ois.jhove.ErrorMessage; +import edu.harvard.hul.ois.jhove.ExternalSignature; +import edu.harvard.hul.ois.jhove.Identifier; +import edu.harvard.hul.ois.jhove.IdentifierType; +import edu.harvard.hul.ois.jhove.InfoMessage; +import edu.harvard.hul.ois.jhove.InternalSignature; +import edu.harvard.hul.ois.jhove.ModuleBase; +import edu.harvard.hul.ois.jhove.OutputHandler; +import edu.harvard.hul.ois.jhove.Property; +import edu.harvard.hul.ois.jhove.PropertyType; +import edu.harvard.hul.ois.jhove.RepInfo; +import edu.harvard.hul.ois.jhove.Signature; +import edu.harvard.hul.ois.jhove.SignatureType; +import edu.harvard.hul.ois.jhove.SignatureUseType; +import it.eng.jhove.*; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; +import java.util.zip.CRC32; + +/* + * This is a module for Jhove - JSTOR/Harvard Object Validation + * Environment + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program 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. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + * + */ + +/** + * Class PngModule A module for Jhove - JSTOR/Harvard + * Object Validation Environment intended to recognize and validate + * PNG files formatted according to the W3C Functional + * specification. ISO/IEC 15948:2003 (E) + * + * This implementation lacks the control of IDATA content against the + * palette size + * + * For some reason, it needs java.util.zip.CRC32 for the CRC + * computation instead of using Jhove own. + * + * Created: Mon Sep 25 12:07:29 2006 + * + * @author Gian Uberto Lauri + * @version $Revision$ + */ + +// TODO write the IDATA decompression algorithm to validate the +// contents against the palette size. +// This requires placing the IDATA data on a separate stream in +// order to rebuild the compressed stream. A temporary file is +// advisable since the data size could grow to invade the core of even +// the stronger application servers. +public class PngModule extends ModuleBase { + + public static final boolean PNG_ENDIANITY=true; + /** + * Crea una nuova istanza di PngModule . + * + */ + public PngModule() { + super (NAME, RELEASE, DATE, FORMAT, COVERAGE, MIMETYPE, WELLFORMED, + VALIDITY, REPINFO, NOTE, RIGHTS, RANDOM); + + keywordList = new HashMap(); + + keywordList.put("Title", + new Booolean(false,"Title")); // Short (one line) title or caption for image + keywordList.put("Author", + new Booolean(false,"Author")); // Name of image's creator + keywordList.put("Description", + new Booolean(false,"Description")); // Description of image (possibly long) + keywordList.put("Copyright", + new Booolean(false,"Copyright")); // Copyright notice + keywordList.put(CREATION_TIME_KEYWORD, + new Booolean(false,CREATION_TIME_KEYWORD)); // Time of original image creation + keywordList.put("Software", + new Booolean(false,"Software")); // Software used to create the image + keywordList.put("Disclaimer", + new Booolean(false,"Disclaimer")); // Legal disclaimer + keywordList.put("Warning", + new Booolean(false,"Warning")); // Warning of nature of content + keywordList.put("Source", + new Booolean(false,"Source")); // Device used to create the image + keywordList.put("Comment", + new Booolean(false,"Comment")); // Miscellaneous comment + } + // Implementation of edu.harvard.hul.ois.jhove.Module + + /** + * init + * + * @param string a String + * @exception Exception + */ + public final void init(final String string) throws Exception { + + } + + /** + * initParse initializes the status of the parser. + * + */ + public final void initParse() { + super.initParse(); + expectingIHDR = RepInfo.TRUE; + expectingPLTE = RepInfo.UNDETERMINED; + expectingIDAT = RepInfo.TRUE; + expectingIEND = RepInfo.TRUE; + expecting_cHRM = RepInfo.UNDETERMINED; + expecting_gAMA = RepInfo.UNDETERMINED; + expecting_iCCP = RepInfo.UNDETERMINED; + expecting_sBIT = RepInfo.UNDETERMINED; + expecting_sRGB = RepInfo.UNDETERMINED; + expecting_tEXt = RepInfo.UNDETERMINED; + expecting_zTXt = RepInfo.UNDETERMINED; + expecting_iTXt = RepInfo.UNDETERMINED; + expecting_bKGD = RepInfo.UNDETERMINED; + expecting_hIST = RepInfo.UNDETERMINED; + expecting_pHYs = RepInfo.UNDETERMINED; + expecting_sPLT = RepInfo.UNDETERMINED; + expecting_tIME = RepInfo.UNDETERMINED; + expecting_tRNS = RepInfo.UNDETERMINED; + + paletteSize = 0; + maxPaletteSize = 0; + shortPalette = false; + colorDepth = 0; + + for (Iterator i = keywordList.values().iterator(); i.hasNext(); ) { + ((Booolean) i.next()).setFlag(false); + } + + } + + /** + * parse Parse the content of a stream PNG image and + * store the results in RepInfo + * + * @param inputStream an InputStream An InputStream, + * positioned at its beginning, which is generated from the object + * to be parsed. If multiple calls to parse are made + * on the basis of a nonzero value being returned, a new + * InputStream must be provided each time. + * @param repInfo a RepInfo A fresh (on the first + * call) RepInfo object which will be modified to reflect the + * results of the parsing If multiple calls to parse are made on + * the basis of a nonzero value being returned, the same RepInfo + * object should be passed with each call. + * @param n an int Must be 0 in first call to + * parse. If parse returns a nonzero + * value, it must be called again with parseIndex equal to that + * return value. + * @return an int + * @exception IOException + */ + public final int parse(final InputStream inputStream, + final RepInfo repInfo, + final int n) throws IOException { + + StringBuffer sigName = new StringBuffer(); + + initParse(); + + // I have to pass it to each method, WHY SHOULD I USE a class + // instance ??? + DataInputStream dstream = getBufferedDataStream (inputStream, + _app != null ? + _je.getBufferSize () : 0); + Agent agent = new Agent ("Harvard University Library", + AgentType.EDUCATIONAL); + agent.setAddress ("Engineering Ingegneria Informatica S.p.a., " + + "Direzione Supporto e Servizi Tecnologici, " + + "Corso Stati Uniti 23/A, 25100 Padova."); + agent.setTelephone ("+39 (49) 8283-411"); + agent.setEmail("saint@eng.it"); + _vendor = agent; + + Document doc = new Document ("PNG (Portable Network Graphics): a file format (pronounced \"ping\"), for a lossless, portable, compressed individual computer graphics image transmitted across the Internet. Indexed-colour, greyscale, and truecolour images are supported, with optional transparency", + DocumentType.REPORT); + agent = new Agent ("W3 Consortium", + AgentType.STANDARD); + agent.setAddress ("E.M.E.A.: ERCIM, 2004 route des Lucioles, BP 93, 06902 Sophia-Antipolis Cedex, France\nJapan & Korea: Keio University, 5322 Endo, Fujisawa, Kanagawa 252-8520 Japan\nAll other countries: MIT, 32 Vassar Street, Room 32-G515, Cambridge, MA 02139 USA"); + agent.setTelephone ("E.M.E.A. : +33.4.92.38.75.90\nJapan & Korea: +81.466.49.1170\nAll other countries: +1.617.253.2613"); + agent.setWeb ("http://www.w3.org/"); + doc.setAuthor (agent); + doc.setDate ("2003-11-10"); + doc.setIdentifier (new Identifier ("http://www.w3.org/Graphics/GIF/spec-gif87.txt", + IdentifierType.URL)); + _specification.add (doc); + + Signature sig = new InternalSignature ("PNG", SignatureType.MAGIC, + SignatureUseType.MANDATORY, 0); + _signature.add (sig); + + sig = new ExternalSignature (".png", SignatureType.EXTENSION, + SignatureUseType.OPTIONAL); + _signature.add (sig); + + _bigEndian = false; + // end of parsing prologue. + if (! checkSignBytes(dstream,SIGNATURE)) { + repInfo.setMessage(new ErrorMessage ("Bad PNG Header", 0)); + repInfo.setWellFormed (RepInfo.FALSE); + return 0; + } + repInfo.setFormat("PNG"); + + // If we got this far, take note that the signature is OK. + repInfo.setSigMatch(_name); + repInfo.setModule(this); + // First chunk MUST be IHDR + int declChunkLen = (int)(readUnsignedInt(dstream, PNG_ENDIANITY, this) + &0x7FFFFFFF); + chcks.reset(); + + int chunkSig = (int)(readUnsignedInt(dstream, PNG_ENDIANITY, this)&0x7FFFFFFF); + chcks.update(int2byteArray(chunkSig)); + + if (chunkSig != IHDR_HEAD_SIG ) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("IHDR header not found where expected." )); + return 0; + } + else { + checkIHDR(dstream,repInfo,declChunkLen); + + if (somethingWrongP(repInfo)) + return 0; + + // not sure it's useful... + expectingIHDR = RepInfo.FALSE; + } + + // The IHDR is where it should be and it's fine, now let's + // handle the other chunks. + + + + while (expectingIEND == RepInfo.TRUE) { + declChunkLen = (int)(readUnsignedInt(dstream, PNG_ENDIANITY, this) + &0x7FFFFFFF); + // Each chunk has its checsum; + chcks.reset(); + + chunkSig = (int)(readUnsignedInt(dstream, PNG_ENDIANITY, this)&0x7FFFFFFF); + chcks.update(int2byteArray(chunkSig)); + + switch (chunkSig) { + + case IHDR_HEAD_SIG: + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Duplicated IHDR chunk." )); + break; + + case PLTE_HEAD_SIG: + if (expectingPLTE == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Unexpected or duplicated PLTE chunk." )); + + break; + } + checkPLTE(dstream,repInfo,declChunkLen); + expectingPLTE = RepInfo.FALSE; + + break; + + case IDAT_HEAD_SIG: + if (expectingPLTE == RepInfo.TRUE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Expected PLTE chunk not found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"IDAT"); + expectingIDAT = RepInfo.FALSE; + break; + + case IEND_HEAD_SIG: + if (expectingPLTE == RepInfo.TRUE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Expected PLTE chunk not found." )); + + break; + } + if (expectingIDAT == RepInfo.TRUE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("No IDAT chunk found." )); + + break; + } + checkChunk(dstream, repInfo, declChunkLen,"IEND"); + expectingIEND = RepInfo.FALSE; + break; + + case cHRM_HEAD_SIG: + if (expectingPLTE == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("cHRM chunk found after PLTE one." )); + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("cHRM chunk found after IDAT ones." )); + + + break; + } + if (expecting_cHRM == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra cHRM chunk found." )); + + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"cHRM"); + expecting_cHRM = RepInfo.FALSE; + + break; + + case gAMA_HEAD_SIG: + if (expectingPLTE == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("gAMA chunk found after PLTE one." )); + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("gAMA chunk found after IDAT ones." )); + + break; + } + if (expecting_gAMA == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra gAMA chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"gAMA"); + expecting_gAMA = RepInfo.FALSE; + + break; + + case iCCP_HEAD_SIG: + + if (expectingPLTE == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("iCCP chunk found after PLTE one." )); + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("iCCP chunk found after IDAT ones." )); + + break; + } + if (expecting_iCCP == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra iCCP chunk found." )); + + break; + } + if (expecting_sRGB == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("iCCP chunk with sRGB chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"iCCP"); + expecting_iCCP = RepInfo.FALSE; + expecting_sRGB = RepInfo.FALSE; + break; + + case sBIT_HEAD_SIG: + if (expectingPLTE == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("sBIT chunk found after PLTE one." )); + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("sBIT chunk found after IDAT ones." )); + + break; + } + if (expecting_sBIT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra sBIT chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"sBIT"); + expecting_sBIT = RepInfo.FALSE; + + + break; + + case sRGB_HEAD_SIG: + if (expectingPLTE == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("sRGB chunk found after PLTE one." )); + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("sRGB chunk found after IDAT ones." )); + + break; + } + if (expecting_sRGB == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra sRGB chunk found." )); + + break; + } + if (expecting_iCCP == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("iCPP chunk after sRGB chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"sRGB"); + expecting_sRGB = RepInfo.FALSE; + expecting_iCCP = RepInfo.FALSE; + + break; + + case tEXt_HEAD_SIG: + + checkChunk(dstream, repInfo, declChunkLen,"tEXT"); + break; + + case zTXt_HEAD_SIG: + + checkChunk(dstream, repInfo, declChunkLen,"zEXT"); + break; + + case iTXt_HEAD_SIG: + + checkChunk(dstream, repInfo, declChunkLen,"iEXT"); + break; + + case bKGD_HEAD_SIG: + + if (expectingPLTE == RepInfo.TRUE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("gAMA chunk found before PLTE one." )); + + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("gAMA chunk found after IDAT ones." )); + + break; + } + if (expecting_gAMA == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra gAMA chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"bKGRD"); + expecting_gAMA = RepInfo.FALSE; + break; + + case hIST_HEAD_SIG: + + if (expectingPLTE == RepInfo.TRUE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("hIST chunk found before PLTE one." )); + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("hIST chunk found after IDAT ones." )); + + break; + } + if (expecting_hIST == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra hIST chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"hIST"); + expecting_hIST = RepInfo.FALSE; + break; + + case tRNS_HEAD_SIG: + + if (expectingPLTE == RepInfo.TRUE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("tRNS chunk found before PLTE one." )); + + break; + } + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("tRNS chunk found after IDAT ones." )); + + break; + } + if (expecting_tRNS == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra tRNS chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"tRNS"); + expecting_tRNS = RepInfo.FALSE; + break; + + case pHYs_HEAD_SIG: + + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("pHYs chunk found after IDAT ones." )); + + break; + } + if (expecting_pHYs == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra pHYs chunk found." )); + + break; + } + + checkChunk(dstream, repInfo, declChunkLen,"pHYs"); + expecting_pHYs = RepInfo.FALSE; + break; + + case sPLT_HEAD_SIG: + + if (expectingIDAT == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("sPLT chunk found after IDAT ones." )); + + break; + } + checkChunk(dstream, repInfo, declChunkLen,"sPLT"); + + break; + + case tIME_HEAD_SIG: + if (expecting_tIME == RepInfo.FALSE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Extra tIME chunk found." )); + + break; + } + + checktIME(dstream, repInfo, declChunkLen); + expecting_tIME = RepInfo.FALSE; + break; + + default: + // Some strong choices. Reject undefined non ancillary chunks + if (( chunkSig & ( PROP_ANCILLARY ) ) == 0) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Unknown non ancillary chunk found. Datastream not interpretable!" )); + + break; + + } + + sigName.delete(0, sigName.length()); + + sigName.append((char)((chunkSig & 0x7F000000) >>> 24) ); + sigName.append((char)((chunkSig & 0x007F0000) >>> 16) ); + sigName.append((char)((chunkSig & 0x00007F00) >>> 8) ); + sigName.append((char)((chunkSig & 0x0000007F)) ); + // a private chunk, check the CRC + checkChunk(dstream, repInfo, declChunkLen, sigName.toString()); + break; + } + + + if (somethingWrongP(repInfo)) + return 0; + + } + + + // epilogue + if (expectingPLTE == RepInfo.TRUE) { + repInfo.setWellFormed(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("PLTE chunk not found (but was expected)." )); + + } + + + return 0; + } + + /** + * parse + * + * @param randomAccessFile a RandomAccessFile + * @param repInfo a RepInfo + * @exception IOException + */ + public final void parse(final RandomAccessFile randomAccessFile, + final RepInfo repInfo) throws IOException { + // Not implemented. + } + + /** + * applyDefaultParams + * + * @exception Exception + */ + public final void applyDefaultParams() throws Exception { + + } + + /** + * resetParams + * + * @exception Exception + */ + public final void resetParams() throws Exception { + + } + + /** + * param + * + * @param string a String + * @exception Exception + */ + public final void param(final String string) throws Exception { + + } + + /** + * setVerbosity + * + * @param n an int + */ + public final void setVerbosity(final int n) { + + } + + /** + * getDefaultParams + * + * @return a List + */ + public final List getDefaultParams() { + return null; + } + + /** + * checkSignatures + * + * @param file a File + * @param inputStream an InputStream + * @param repInfo a RepInfo + * @exception IOException + */ + public final void checkSignatures(final File file, + final InputStream inputStream, + final RepInfo repInfo) throws IOException { + DataInputStream dis = getBufferedDataStream (inputStream, _app != null ? + _je.getBufferSize () : 0); + if (! checkSignBytes(dis,SIGNATURE)) { + repInfo.setConsistent (false); + return; + } + } + + /** + * checkSignatures Not used + * + * @param file a File + * @param stream a InputStream + * @param repInfo a RepInfo + * @exception IOException + */ + public final void checkSignatures(final File file, + final RandomAccessFile stream, + final RepInfo repInfo) + throws IOException { + // Not used. + } + + /** + * show + * + * @param outputHandler an OutputHandler + */ + public final void show(final OutputHandler outputHandler) { + + } + + + private final boolean checkSignBytes(final DataInputStream inputStream, + final int sigBytes[]) + throws IOException { + int max = sigBytes.length; + int c; + boolean rv = true; + + for (int i = 0; i < max; i++) { + c = readUnsignedByte(inputStream,this); + rv &= (c == sigBytes[i]); + } + + return rv; + } + + + private final void checkIHDR(final DataInputStream inputStream, + final RepInfo repInfo, + final int declChunkLen) + throws IOException { + + // W3C recommendations states that height and width are integers that + // range from 0 to 2^31 + int tmp = (int)(readUnsignedInt(inputStream, PNG_ENDIANITY, this)&0xFFFFFFFF); + chcks.update(int2byteArray(tmp)); + + if (tmp == 0) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Illegal 0 value for height." )); + + } + + tmp = (int)(readUnsignedInt(inputStream, PNG_ENDIANITY, this)&0xFFFFFFFF); + chcks.update(int2byteArray(tmp)); + + if (tmp == 0) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Illegal 0 value for width." )); + + } + + tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + + int colorType = readUnsignedByte(inputStream, this); + chcks.update((byte)colorType); + + switch (colorType) { + case 0: + if (tmp != 1 && + tmp != 2 && + tmp != 4 && + tmp != 8 && + tmp != 16) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("In IHDR, illegal value for bit depth for colour type " + + + colorType + ": " +tmp )); + + } + repInfo.setProfile("PNG GrayScale"); + + expectingPLTE=RepInfo.FALSE; + case 3: + if (tmp != 1 && + tmp != 2 && + tmp != 4 && + tmp != 8 ) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("In IHDR, illegal value for bit depth for colour type " + + colorType + ": " +tmp )); + + } + // We need to find a palette! + expectingPLTE = RepInfo.TRUE; + colorDepth = tmp; + maxPaletteSize = 1 << tmp ; + repInfo.setProfile("PNG Indexed"); + + break; + case 4: + expectingPLTE=RepInfo.FALSE; + if (tmp != 8 && + tmp != 16) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("In IHDR, valore illegale per la profondita` dei bit per il colour type " + + colorType + ": " +tmp )); + + } + + repInfo.setProfile("PNG GrayScale with Alpha"); + break; + case 6: + expectingPLTE=RepInfo.FALSE; + expecting_tRNS=RepInfo.FALSE; + if (tmp != 8 && + tmp != 16) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("In IHDR, valore illegale per la profondita` dei bit per il colour type " + + colorType + ": " +tmp )); + + } + repInfo.setProfile("PNG Truecolor with Alpha"); + break; + case 2: + expectingPLTE=RepInfo.FALSE; + expecting_tRNS=RepInfo.FALSE; + if (tmp != 8 && + tmp != 16) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("In IHDR, valore illegale per la profondita` dei bit per il colour type " + + colorType + ": " +tmp )); + + } + + repInfo.setProfile("PNG Truecolor"); + break; + default: + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("In IHDR, valore illegale per il colour type (" + + colorType +")")); + + break; + } + + // Compression + tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + + if (tmp!=0) { + repInfo.setMessage(new InfoMessage("Attenzione, tipo di compressine " + + tmp + " not conforme alla raccommandazione del W3C.")); + } + + + // filtering + tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + + if (tmp!=0) { + repInfo.setMessage(new InfoMessage("Attenzione, tipo di filtro " + + tmp + " no ancora standardizzato dal W3C.")); + } + + // interlace + + tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + + if (tmp!=0 && tmp!=1) { + repInfo.setMessage(new InfoMessage("Attenzione, tipo di interlacciamento " + + tmp + " no ancora standardizzato dal W3C.")); + } + + long crc32 = readUnsignedInt(inputStream, PNG_ENDIANITY, this); + + if (crc32 != chcks.getValue()) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Errore CRC nel chunk IHDR" )); + } + + } + + private final void checkPLTE(final DataInputStream inputStream, + final RepInfo repInfo, + final int declChunkLen) + throws IOException { + + if ((declChunkLen % 3) != 0) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Lunghezza PLTE non valida." )); + } + + + // Scan the palette + paletteSize=0; + + for (int i = 0; i < declChunkLen; i++) { + int tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + + tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + + tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + + paletteSize++; + + } + + if (paletteSize > maxPaletteSize) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Too many palette items in PLTE chunk" )); + + + } + shortPalette = (paletteSize < maxPaletteSize); + long crc32 = readUnsignedInt(inputStream, PNG_ENDIANITY, this); + + if (crc32 != chcks.getValue()) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("CRC Error in PLTE chunk" )); + + } + } + + private final void checktIME(final DataInputStream inputStream, + final RepInfo repInfo, + final int declChunkLen) + throws IOException { + int yhrHigh = readUnsignedByte(inputStream, this); + chcks.update((byte)yhrHigh); + + int yhrLow = readUnsignedByte(inputStream, this); + chcks.update((byte)yhrLow); + + int month = readUnsignedByte(inputStream, this); + chcks.update((byte)month); + + if (month < 1 || month > 12) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Illegal month value in tIME chunk")); + + } + + int day = readUnsignedByte(inputStream, this); + chcks.update((byte)day); + + if (day < 1 || day > 31) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Illegal day value in tIME chunk")); + + } + + int hour = readUnsignedByte(inputStream, this); + chcks.update((byte)hour); + + if (hour < 0 || hour > 23) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Illegal hour value in tIME chunk")); + + } + + int minute = readUnsignedByte(inputStream, this); + chcks.update((byte)minute); + + if (minute < 0 || minute > 59) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Illegal minute value in tIME chunk")); + + } + + int second = readUnsignedByte(inputStream, this); + chcks.update((byte)second); + + if (second < 0 || second > 60) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Illegal second value in tIME chunk")); + + } + + long crc32 = readUnsignedInt(inputStream, PNG_ENDIANITY, this); + + if (crc32 != chcks.getValue()) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("CRC Error in tIME chunk" )); + + } + + // Some operations to deal with GregorianCalendar class, that + // has 0 base month numbering and doesn't like leap seconds + second = (second == 60) ? 59 : second; + month--; + repInfo.setLastModified(new GregorianCalendar((yhrHigh<<8)+yhrLow , + month, + day, + hour, + minute, + second).getTime()); + + } + + + private final void checktEXT(final DataInputStream inputStream, + final RepInfo repInfo, + int declChunkLen) + throws IOException { + int c=-1; + int keywordLen=0; + StringBuffer buf = new StringBuffer(); + + while (keywordLen < MAX_KEYWORD_LEN) { + c = readUnsignedByte(inputStream, this); + chcks.update((byte)c); + + declChunkLen--; + + if (c==0) { + break; + } + + keywordLen++; + buf.append((char)c); + } + + + if (keywordLen == MAX_KEYWORD_LEN) { + // we hit MAX_KEYWORD_LEN, let's check if there's the + // mandatory 0 byte + c = readUnsignedByte(inputStream, this); + chcks.update((byte)c); + declChunkLen--; + + if (c != 0) { + // segnalare errore e scartare + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("Missing 0 byte after keyword")); + + checkChunk(inputStream, repInfo, declChunkLen, "tEXT"); + buf.append((char)c); + + } + + + return; + } + + String keyword = buf.toString(); + + // so far we got a keyword and the null ( 0 ) separator, lets' + // get the value, set a property and check that everything is + // OK with the CRC. + + buf.delete(0, buf.length() ); + + while (declChunkLen > 0) { + c = readUnsignedByte(inputStream, this); + chcks.update((byte)c); + declChunkLen--; + + } + + String value = buf.toString(); + + Property p = new Property(keyword, + PropertyType.STRING, + value); + repInfo.setProperty(p); + Booolean bol = (Booolean)keywordList.get(keyword); + + if (bol != null) { + bol.setFlag(true); + } + + long crc32 = readUnsignedInt(inputStream, PNG_ENDIANITY, this); + + if (crc32 != chcks.getValue()) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("CRC Error in tEXT chunk" )); + + } + + + } + + private final void checkChunk(final DataInputStream inputStream, + final RepInfo repInfo, + int declChunkLen, + final String chunkSig) + throws IOException { + int read = 0; + + while (read < declChunkLen) { + int tmp = readUnsignedByte(inputStream, this); + chcks.update((byte)tmp); + read++; + } + long crc32 = readUnsignedInt(inputStream, PNG_ENDIANITY, this); + + if (crc32 != chcks.getValue()) { + repInfo.setValid(RepInfo.FALSE); + repInfo.setMessage(new ErrorMessage("CRC Error in " + chunkSig +" chunk" )); + + } + + } + + // Utility predicate + private final boolean somethingWrongP(RepInfo repInfo) { + return repInfo.getWellFormed() == RepInfo.FALSE || + repInfo.getValid() == RepInfo.FALSE; + } + + private final String getCompressedString(DataInputStream dis, + int bytesToRead) + throws IOException, DataFormatException { + ByteArrayOutputStream bous = new ByteArrayOutputStream(); + Inflater pump = new Inflater(); + byte o[] = new byte[1024]; + + while (bytesToRead > 0) { + int c = 0; + + while (c < 1024 && bytesToRead > 0) { + o[c++] = (byte)(readUnsignedByte(dis,this)); + bytesToRead--; + } + + pump.setInput(o); + + c=1; + while (!pump.needsInput() && c > 0) { + c = pump.inflate(o); + + if (c>0) + bous.write(o,0,c); + } + + + } + + return bous.toString("ISO-8859-1"); + + } + + // Turns an 32 bit integer intto a byte array + private final byte[] int2byteArray(int a) + { + byte b[]=new byte[]{0,0,0,0}; + + b[3]=(byte)(a&0xFF); + a=a>>8; + + b[2]=(byte)(a&0xFF); + a=a>>8; + + b[1]=(byte)(a&0xFF); + a=a>>8; + + b[0]=(byte)(a&0xFF); + + return b; + + } + + +// private final Checksummer chcks = new Checksummer(); + + // Strangely, it seems it doesn't work with the other checksummer... + private final CRC32 chcks = new CRC32(); + + // PNG signature + private static final int SIGNATURE[] = {137, 80, 78, 71, 13, 10, 26, 10}; + + // Module instantiation constants. + private static final String NAME = "PNG-engineering"; + private static final String RELEASE = "1.0"; + private static final int DATE[] = {2006, 9, 25}; + private static final String FORMAT[] = { + "PNG", + "Portable Network Graphics" + }; + private static final String MIMETYPE[] = {"image/png"}; + private static final String COVERAGE = "PNG"; + private static final String WELLFORMED = null; + private static final String VALIDITY = null; + private static final String REPINFO = null; + private static final String NOTE = "Work in progress"; + private static final String RIGHTS = + "Copyright 2006 Engineering Ingengeria Informatica S.p.a." + + "Released under the GNU Lesser General Public License." + + "Cryptoserver Library Copyright Engiweb Security, all rights reserved"; + private static final boolean RANDOM = false; + + /* + * Chunk signatures. + * + * Java *IS* Big Endian, PNG chunk signatures are 4 byte strings we + * *CAN* read into an int variable since all of them have bit 7 + * set to 0. + * + * Therefore we can check each chunk signature against int + * constants (one opcode executed, no loops). + * + * About names: these name violate the Java naming rules for + * constants, but I prefer to keep the PNG chunk name cases, since + * they are meaningful for the properties of each chunk. + */ + private final static int IHDR_HEAD_SIG = 0x49484452; + private final static int PLTE_HEAD_SIG = 0x504c5445; + private final static int IDAT_HEAD_SIG = 0x49444154; + private final static int IEND_HEAD_SIG = 0x49454e44; + private final static int cHRM_HEAD_SIG = 0x6348524d; + private final static int gAMA_HEAD_SIG = 0x67414d41; + private final static int iCCP_HEAD_SIG = 0x69434350; + private final static int sBIT_HEAD_SIG = 0x73424954; + private final static int sRGB_HEAD_SIG = 0x73524742; + private final static int tEXt_HEAD_SIG = 0x74455874; + private final static int zTXt_HEAD_SIG = 0x7a545874; + private final static int iTXt_HEAD_SIG = 0x69545874; + private final static int bKGD_HEAD_SIG = 0x624b4744; + private final static int hIST_HEAD_SIG = 0x68495354; + private final static int pHYs_HEAD_SIG = 0x70485973; + private final static int sPLT_HEAD_SIG = 0x73504c54; + private final static int tIME_HEAD_SIG = 0x74494d45; + private final static int tRNS_HEAD_SIG = 0x74524e53; + // Property bit masks + private final static int PROP_SAFE_TO_COPY = 0x00000020; + private final static int PROP_PRIVATE = 0x00002000; + private final static int PROP_RESERVED = 0x00200000; + private final static int PROP_ANCILLARY = 0x20000000; + + // Maximum keyword lenght + private final static int MAX_KEYWORD_LEN = 79; + + // Standard keyword for the creation timestamp + private final static String CREATION_TIME_KEYWORD = "Creation Time"; + + /*------------------------------------------------------------------* + |******************************************************************| + |* *| + |* Parser inner state flags. *| + |* *| + |* The state is represented by a score of flags associated to the *| + |* chunks that should appear no more than once. The code uses the *| + |* flags to manage the partial ordering in the chunk layout *| + |* *| + |* Flags have the value RepInfo.UNDETERMINED when the chunk may *| + |* either appear or not, RepInfo.TRUE when the chunk is expected *| + |* but has yet to be found, RepInfo.FALSE when the chunk should *| + |* not appear any more. Istantiation values are for documentation *| + |* purpose only and are repeated in the initParse() method. *| + |* *| + |******************************************************************| + *------------------------------------------------------------------*/ + + /* + * Starting chunk, must be the first one, expected. + */ + private int expectingIHDR = RepInfo.TRUE; + + /* + * This is 3 state flag: it is unknown until you know you need to + * find the PLTE chunk, when it turs to RepInfo.TRUE or/and you + * know you should not find such block any more (i.e. from color + * type and bit depth or because you already got this chunk. + */ + private int expectingPLTE = RepInfo.UNDETERMINED; + + /* + * Data chunk, turns to RepInfo.false upon finding the firs chunk + * of this type + */ + private int expectingIDAT = RepInfo.TRUE; + + /* + * Ending chunk, this flag is used to handle the end of file + * condition, if it happens when the flag is RepInfo.TRUE; then + * the file is not well formed. + */ + private int expectingIEND = RepInfo.TRUE; + + // non critical chunks + + private int expecting_cHRM = RepInfo.UNDETERMINED; + private int expecting_gAMA = RepInfo.UNDETERMINED; + private int expecting_iCCP = RepInfo.UNDETERMINED; + private int expecting_sBIT = RepInfo.UNDETERMINED; + private int expecting_sRGB = RepInfo.UNDETERMINED; + private int expecting_tEXt = RepInfo.UNDETERMINED; + private int expecting_zTXt = RepInfo.UNDETERMINED; + private int expecting_iTXt = RepInfo.UNDETERMINED; + private int expecting_bKGD = RepInfo.UNDETERMINED; + private int expecting_hIST = RepInfo.UNDETERMINED; + private int expecting_pHYs = RepInfo.UNDETERMINED; + private int expecting_sPLT = RepInfo.UNDETERMINED; + private int expecting_tIME = RepInfo.UNDETERMINED; + private int expecting_tRNS = RepInfo.UNDETERMINED; + + // Palette size, in colours + private int maxPaletteSize = 0; + private int paletteSize = 0; + private boolean shortPalette = false; + private int colorDepth = 0; + + private Map keywordList; + + private final static String PNG_PROFILES[] = + new String[] { "PNG GrayScale", // 0 + "Unused", // 1 + "PNG Truecolor", // 2 + "PNG Indexed", // 3 + "PNG GrayScale with Alpha", // 4 + "Unused", // 5 + "PNG Truecolor with Alpha"}; // 6 + +}