/* * Copyright 2016 Skynav, Inc. All rights reserved. * Portions Copyright 2009 Extensible Formatting Systems, Inc (XFSI). * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY SKYNAV, INC. AND ITS CONTRIBUTORS “AS IS” AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL SKYNAV, INC. OR ITS CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.xfsi.xav.validation.images.jpeg; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.Map; import com.xfsi.xav.test.TestInfo; import com.xfsi.xav.test.TestManager; import com.xfsi.xav.util.Error; import com.xfsi.xav.util.Result; import com.xfsi.xav.validation.util.AbstractLoggingValidator; /** * Parses and validates JPEG files. */ public final class JpegValidator extends AbstractLoggingValidator { static enum MsgCode { JPG01I001, JPG01I002, JPG01I003, JPG01I004, JPG01I005, JPG01I006, JPG01I007, JPG01I008, JPG01I009, JPG01I010, JPG01I011, JPG01I012, JPG01I013, JPG01I014, JPG01I015, JPG01I016, JPG01I017, JPG01I018, JPG01I019, JPG01I020, JPG01I021, JPG01I022, JPG01I023, JPG01I024, JPG01I025, JPG01I026, JPG01I027, JPG01I028, JPG01I029, JPG01I030, JPG01I031, JPG01I032, JPG01I033, JPG01I034, JPG01I035, JPG01I036, JPG01I037, JPG01I038, JPG01I039, JPG01X001, JPG01X002, JPG01X003, JPG01X004, JPG01X005, JPG01X006, JPG01W001, JPG01W002, JPG01W003, JPG01W004, JPG01W005, JPG01W006, JPG01W007, JPG01W008, JPG01W009, JPG01W010, JPG01W011, JPG01W012, JPG01W013, JPG01W014, JPG01W015, JPG01W016, JPG01E001, JPG01E002, JPG01F001, JPG01F002, JPG01F003, JPG01E004, JPG01E005, JPG01E006, JPG01E007, JPG01E009, JPG01E010, JPG01E011, JPG01E012, JPG01E013, JPG01E014, JPG01E015, JPG01E016, JPG01E017, JPG01E018, JPG01E019, JPG01E020, JPG01E021, JPG01E022, JPG01E024, JPG01E025, JPG01E026, JPG01E027, JPG01E028, JPG01E029, JPG01E030, JPG01E031, JPG01E032, JPG01E033, JPG01E034, JPG01E035, JPG01E036, JPG01E037, JPG01E038, JPG01E039, JPG01E040, JPG01E041, JPG01E042, JPG01E043, JPG01E044, JPG01E045, JPG01E046, JPG01E047, JPG01E048, } private JpegInputStream inputStream = null; private JpegState state = null; public JpegValidator() { super(Error.TestType.STATIC, Error.ContentType.IMAGE_JPG); } public Result run(TestManager tm, TestInfo ti) throws Exception { initState(tm,ti); Map<String,Object> resultState = new java.util.HashMap<String,Object>(); validate(resultState); Result r = getErrorReported() ? Result.FAIL : Result.PASS; return new Result(r, resultState); } public String getVersion() { // TODO: implement versioning return "1.0.0"; } public void validate(Map<String,Object> resultState) { this.state = new JpegState(resultState); try { InputStream is = getTestInfo().getResourceStream(); assert(is != null) : msgFormatterNV(MsgCode.JPG01X002.toString()); logAll(MsgCode.JPG01I001); this.inputStream = new JpegInputStream(is); if (dispatchSegmentParser()) assertEOF(); } catch (EOFException e) { // Nothing to do, terminating early due to EOF } catch (AssertionError e) { logProgress(MsgCode.JPG01X001, e.getMessage(), this.inputStream.getTotalBytesRead()); } logAll(MsgCode.JPG01I002); } private boolean dispatchSegmentParser() throws EOFException { while (!this.state.isEoiFound()) { if (!findParserClassNameForSegment()) return false; if (!parseSegment()) return false; if (!assertSoiSegmentIsFirst()) return false; } return performFinalChecks(); } private boolean performFinalChecks() { checkAbbreviatedFormat(); assertAtLeastOneFrameSegmentFound(); assertOneOrMoreSosSegmentFound(); assertDnlSegmentFoundIfRequired(); assertApp0SegmentFound(); return assertEoiSegmentIsLast(); } private boolean parseSegment() throws EOFException { String className = this.state.getCurrentSegmentParserName(); try { Class<?> c = Class.forName(className); SegmentParser mp = (SegmentParser) c.newInstance(); return mp.validate(this.inputStream, this.state, this); } catch (ClassNotFoundException e) { Integer code = this.state.getCurrentCode(); if (!isReservedApplicationSegment(code)) { String symbol; if ((symbol = findReservedJpegExtensionSymbol(code)) != null) logResult(MsgCode.JPG01E006, code, symbol, this.inputStream.getTotalBytesRead()); else if (code == 0xff01) logResult(MsgCode.JPG01E007, this.inputStream.getTotalBytesRead()); else if (code >= 0xff02 && code <= 0xffbf) logResult(MsgCode.JPG01E002, code, this.inputStream.getTotalBytesRead()); else { logResult(MsgCode.JPG01E014, code, this.inputStream.getTotalBytesRead()); return false; } skipToNextSegment(); } return true; } catch (IllegalAccessException e) { logProgress(MsgCode.JPG01X005, Thread.currentThread().getStackTrace()[2].getMethodName(), className, e.getMessage(), this.inputStream.getTotalBytesRead()); } catch (InstantiationException e) { logProgress(MsgCode.JPG01X006, Thread.currentThread().getStackTrace()[2].getMethodName(), className, e.getMessage(), this.inputStream.getTotalBytesRead()); } return false; } private boolean findParserClassNameForSegment() throws EOFException { try { byte b; // get rid of 0xff optionally preceding the code do { b = this.inputStream.readByte(); } while ((b & 0xff) == 0xff); short code = (short) (0xff00 | (b & 0xff)); String markerClassBasePathName = SegmentParser.class.getName(); String segmentParserName = String.format("%1$s%2$X", markerClassBasePathName, code); this.state.setCurrentCode(code); this.state.setCurrentSegmentParserName(segmentParserName); logAll(MsgCode.JPG01I004, this.state.getCurrentCode(), this.state.getSegmentCount()); return true; } catch (EOFException e) { logResult(MsgCode.JPG01F001, this.inputStream.getTotalBytesRead()); throw e; } catch (IOException e) { assert(false) : msgFormatterNV(MsgCode.JPG01X003.toString(), Thread.currentThread().getStackTrace()[2].getMethodName(), e.getMessage()); } return false; } private boolean assertSoiSegmentIsFirst() { if (!this.state.isSoiFound()) { logResult(MsgCode.JPG01E004, this.state.getCurrentCode(), this.inputStream.getTotalBytesRead()); return false; } return true; } private boolean assertEoiSegmentIsLast() { if (this.state.getCurrentCode() != 0xffd9) { logResult(MsgCode.JPG01E005, this.state.getCurrentCode(), this.inputStream.getTotalBytesRead()); return false; } return true; } private void checkAbbreviatedFormat() { if (this.state.getInitialFrameCode() == null && this.state.getTableMiscSegmentCount() > 0) logResult(MsgCode.JPG01W005); } private void assertAtLeastOneFrameSegmentFound() { if (this.state.getInitialFrameCode() == null) logResult(MsgCode.JPG01E010, this.inputStream.getTotalBytesRead()); } private void assertOneOrMoreSosSegmentFound() { if (this.state.getSosSegmentCount() == 0) logResult(MsgCode.JPG01E012, this.inputStream.getTotalBytesRead()); } private void assertDnlSegmentFoundIfRequired() { if (this.state.isDnlSegmentRequired() && this.state.getDnlSegmentCount() == 0) logResult(MsgCode.JPG01E016, this.state.getInitialFrameCode()); } private void assertApp0SegmentFound() { if (this.state.getApp0SegmentCount() == 0) logResult(MsgCode.JPG01E018, this.state.getSegmentCount()); } private void skipToNextSegment() throws EOFException { try { byte b; boolean isMarkerStart = false; while (true) { b = this.inputStream.readByte(); if ((b & 0xff) == 0xff && !isMarkerStart) isMarkerStart = true; else if (isMarkerStart && ((b & 0xff) != 0 || (b & 0xff) != 0xff)) { // next segment marker code found this.inputStream.putBack((byte) 0xff); this.inputStream.putBack(b); break; } } } catch (EOFException e) { logResult(MsgCode.JPG01F001, this.inputStream.getTotalBytesRead()); throw e; } catch (IOException e) { assert(false) : msgFormatterNV(MsgCode.JPG01X003.toString(), Thread.currentThread().getStackTrace()[2].getMethodName(), e.getMessage()); } } private boolean isReservedApplicationSegment(Integer code) throws EOFException { if (code >= 0xffe0 && code <= 0xffef) { this.state.incrementTablesMiscSegmentCount(); String symbol = null; try { short size = this.inputStream.readShort(); symbol = String.format("APP%1$d", code & 0xf); logAll(MsgCode.JPG01I005, code, symbol, size); size -= 2; // already read segment size this.inputStream.skipBytes(size); return true; } catch (EOFException e) { logResult(MsgCode.JPG01F002, code, symbol, this.inputStream.getTotalBytesRead()); throw e; } catch (IOException e) { assert(false) : msgFormatterNV(MsgCode.JPG01X003.toString(), Thread.currentThread().getStackTrace()[2].getMethodName(), e.getMessage()); } } return false; } private String findReservedJpegExtensionSymbol(Integer code) { if (code == 0xffc8) return "JPG"; if (code >= 0xfff0 && code <= 0xfffd) return "JPG" + (code & 0xf); return null; } private void assertEOF() { try { this.inputStream.readByte(); } catch (EOFException e) { logAll(MsgCode.JPG01I003); return; } catch (IOException e) { assert(false) : msgFormatterNV(MsgCode.JPG01X003.toString(), Thread.currentThread().getStackTrace()[2].getMethodName(), e.getMessage()); } logResult(MsgCode.JPG01E001); } }