/* * Copyright (c) 2007 Henri Sivonen * Copyright (c) 2008-2010 Mozilla Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ package nu.validator.htmlparser.impl; import java.io.IOException; import nu.validator.htmlparser.annotation.NoLength; import nu.validator.htmlparser.common.ByteReadable; import org.xml.sax.SAXException; public abstract class MetaScanner { /** * Constant for "charset". */ private static final @NoLength char[] CHARSET = "charset".toCharArray(); /** * Constant for "content". */ private static final @NoLength char[] CONTENT = "content".toCharArray(); private static final int NO = 0; private static final int M = 1; private static final int E = 2; private static final int T = 3; private static final int A = 4; private static final int DATA = 0; private static final int TAG_OPEN = 1; private static final int SCAN_UNTIL_GT = 2; private static final int TAG_NAME = 3; private static final int BEFORE_ATTRIBUTE_NAME = 4; private static final int ATTRIBUTE_NAME = 5; private static final int AFTER_ATTRIBUTE_NAME = 6; private static final int BEFORE_ATTRIBUTE_VALUE = 7; private static final int ATTRIBUTE_VALUE_DOUBLE_QUOTED = 8; private static final int ATTRIBUTE_VALUE_SINGLE_QUOTED = 9; private static final int ATTRIBUTE_VALUE_UNQUOTED = 10; private static final int AFTER_ATTRIBUTE_VALUE_QUOTED = 11; private static final int MARKUP_DECLARATION_OPEN = 13; private static final int MARKUP_DECLARATION_HYPHEN = 14; private static final int COMMENT_START = 15; private static final int COMMENT_START_DASH = 16; private static final int COMMENT = 17; private static final int COMMENT_END_DASH = 18; private static final int COMMENT_END = 19; private static final int SELF_CLOSING_START_TAG = 20; /** * The data source. */ protected ByteReadable readable; /** * The state of the state machine that recognizes the tag name "meta". */ private int metaState = NO; /** * The current position in recognizing the attribute name "content". */ private int contentIndex = -1; /** * The current position in recognizing the attribute name "charset". */ private int charsetIndex = -1; /** * The tokenizer state. */ protected int stateSave = DATA; /** * The currently filled length of strBuf. */ private int strBufLen; /** * Accumulation buffer for attribute values. */ private char[] strBuf; // [NOCPP[ /** * @param source * @param errorHandler * @param publicId * @param systemId */ public MetaScanner() { this.readable = null; this.metaState = NO; this.contentIndex = -1; this.charsetIndex = -1; this.stateSave = DATA; strBufLen = 0; strBuf = new char[36]; } /** * Reads a byte from the data source. * * -1 means end. * @return * @throws IOException */ protected int read() throws IOException { return readable.readByte(); } // ]NOCPP] // WARNING When editing this, makes sure the bytecode length shown by javap // stays under 8000 bytes! /** * The runs the meta scanning algorithm. */ protected final void stateLoop(int state) throws SAXException, IOException { int c = -1; boolean reconsume = false; stateloop: for (;;) { switch (state) { case DATA: dataloop: for (;;) { if (reconsume) { reconsume = false; } else { c = read(); } switch (c) { case -1: break stateloop; case '<': state = MetaScanner.TAG_OPEN; break dataloop; // FALL THROUGH continue // stateloop; default: continue; } } // WARNING FALLTHRU CASE TRANSITION: DON'T REORDER case TAG_OPEN: tagopenloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case 'm': case 'M': metaState = M; state = MetaScanner.TAG_NAME; break tagopenloop; // continue stateloop; case '!': state = MetaScanner.MARKUP_DECLARATION_OPEN; continue stateloop; case '?': case '/': state = MetaScanner.SCAN_UNTIL_GT; continue stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { metaState = NO; state = MetaScanner.TAG_NAME; break tagopenloop; // continue stateloop; } state = MetaScanner.DATA; reconsume = true; continue stateloop; } } // FALL THROUGH DON'T REORDER case TAG_NAME: tagnameloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case ' ': case '\t': case '\n': case '\u000C': state = MetaScanner.BEFORE_ATTRIBUTE_NAME; break tagnameloop; // continue stateloop; case '/': state = MetaScanner.SELF_CLOSING_START_TAG; continue stateloop; case '>': state = MetaScanner.DATA; continue stateloop; case 'e': case 'E': if (metaState == M) { metaState = E; } else { metaState = NO; } continue; case 't': case 'T': if (metaState == E) { metaState = T; } else { metaState = NO; } continue; case 'a': case 'A': if (metaState == T) { metaState = A; } else { metaState = NO; } continue; default: metaState = NO; continue; } } // FALLTHRU DON'T REORDER case BEFORE_ATTRIBUTE_NAME: beforeattributenameloop: for (;;) { if (reconsume) { reconsume = false; } else { c = read(); } /* * Consume the next input character: */ switch (c) { case -1: break stateloop; case ' ': case '\t': case '\n': case '\u000C': continue; case '/': state = MetaScanner.SELF_CLOSING_START_TAG; continue stateloop; case '>': state = DATA; continue stateloop; case 'c': case 'C': contentIndex = 0; charsetIndex = 0; state = MetaScanner.ATTRIBUTE_NAME; break beforeattributenameloop; default: contentIndex = -1; charsetIndex = -1; state = MetaScanner.ATTRIBUTE_NAME; break beforeattributenameloop; // continue stateloop; } } // FALLTHRU DON'T REORDER case ATTRIBUTE_NAME: attributenameloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case ' ': case '\t': case '\n': case '\u000C': state = MetaScanner.AFTER_ATTRIBUTE_NAME; continue stateloop; case '/': state = MetaScanner.SELF_CLOSING_START_TAG; continue stateloop; case '=': strBufLen = 0; state = MetaScanner.BEFORE_ATTRIBUTE_VALUE; break attributenameloop; // continue stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: if (metaState == A) { if (c >= 'A' && c <= 'Z') { c += 0x20; } if (contentIndex == 6) { contentIndex = -1; } else if (contentIndex > -1 && contentIndex < 6 && (c == CONTENT[contentIndex + 1])) { contentIndex++; } if (charsetIndex == 6) { charsetIndex = -1; } else if (charsetIndex > -1 && charsetIndex < 6 && (c == CHARSET[charsetIndex + 1])) { charsetIndex++; } } continue; } } // FALLTHRU DON'T REORDER case BEFORE_ATTRIBUTE_VALUE: beforeattributevalueloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case ' ': case '\t': case '\n': case '\u000C': continue; case '"': state = MetaScanner.ATTRIBUTE_VALUE_DOUBLE_QUOTED; break beforeattributevalueloop; // continue stateloop; case '\'': state = MetaScanner.ATTRIBUTE_VALUE_SINGLE_QUOTED; continue stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: if (charsetIndex == 6 || contentIndex == 6) { addToBuffer(c); } state = MetaScanner.ATTRIBUTE_VALUE_UNQUOTED; continue stateloop; } } // FALLTHRU DON'T REORDER case ATTRIBUTE_VALUE_DOUBLE_QUOTED: attributevaluedoublequotedloop: for (;;) { if (reconsume) { reconsume = false; } else { c = read(); } switch (c) { case -1: break stateloop; case '"': if (tryCharset()) { break stateloop; } state = MetaScanner.AFTER_ATTRIBUTE_VALUE_QUOTED; break attributevaluedoublequotedloop; // continue stateloop; default: if (metaState == A && (contentIndex == 6 || charsetIndex == 6)) { addToBuffer(c); } continue; } } // FALLTHRU DON'T REORDER case AFTER_ATTRIBUTE_VALUE_QUOTED: afterattributevaluequotedloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case ' ': case '\t': case '\n': case '\u000C': state = MetaScanner.BEFORE_ATTRIBUTE_NAME; continue stateloop; case '/': state = MetaScanner.SELF_CLOSING_START_TAG; break afterattributevaluequotedloop; // continue stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: state = MetaScanner.BEFORE_ATTRIBUTE_NAME; reconsume = true; continue stateloop; } } // FALLTHRU DON'T REORDER case SELF_CLOSING_START_TAG: c = read(); switch (c) { case -1: break stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: state = MetaScanner.BEFORE_ATTRIBUTE_NAME; reconsume = true; continue stateloop; } // XXX reorder point case ATTRIBUTE_VALUE_UNQUOTED: for (;;) { if (reconsume) { reconsume = false; } else { c = read(); } switch (c) { case -1: break stateloop; case ' ': case '\t': case '\n': case '\u000C': if (tryCharset()) { break stateloop; } state = MetaScanner.BEFORE_ATTRIBUTE_NAME; continue stateloop; case '>': if (tryCharset()) { break stateloop; } state = MetaScanner.DATA; continue stateloop; default: if (metaState == A && (contentIndex == 6 || charsetIndex == 6)) { addToBuffer(c); } continue; } } // XXX reorder point case AFTER_ATTRIBUTE_NAME: for (;;) { c = read(); switch (c) { case -1: break stateloop; case ' ': case '\t': case '\n': case '\u000C': continue; case '/': if (tryCharset()) { break stateloop; } state = MetaScanner.SELF_CLOSING_START_TAG; continue stateloop; case '=': state = MetaScanner.BEFORE_ATTRIBUTE_VALUE; continue stateloop; case '>': if (tryCharset()) { break stateloop; } state = MetaScanner.DATA; continue stateloop; case 'c': case 'C': contentIndex = 0; charsetIndex = 0; state = MetaScanner.ATTRIBUTE_NAME; continue stateloop; default: contentIndex = -1; charsetIndex = -1; state = MetaScanner.ATTRIBUTE_NAME; continue stateloop; } } // XXX reorder point case MARKUP_DECLARATION_OPEN: markupdeclarationopenloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case '-': state = MetaScanner.MARKUP_DECLARATION_HYPHEN; break markupdeclarationopenloop; // continue stateloop; default: state = MetaScanner.SCAN_UNTIL_GT; reconsume = true; continue stateloop; } } // FALLTHRU DON'T REORDER case MARKUP_DECLARATION_HYPHEN: markupdeclarationhyphenloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case '-': state = MetaScanner.COMMENT_START; break markupdeclarationhyphenloop; // continue stateloop; default: state = MetaScanner.SCAN_UNTIL_GT; reconsume = true; continue stateloop; } } // FALLTHRU DON'T REORDER case COMMENT_START: commentstartloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case '-': state = MetaScanner.COMMENT_START_DASH; continue stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: state = MetaScanner.COMMENT; break commentstartloop; // continue stateloop; } } // FALLTHRU DON'T REORDER case COMMENT: commentloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case '-': state = MetaScanner.COMMENT_END_DASH; break commentloop; // continue stateloop; default: continue; } } // FALLTHRU DON'T REORDER case COMMENT_END_DASH: commentenddashloop: for (;;) { c = read(); switch (c) { case -1: break stateloop; case '-': state = MetaScanner.COMMENT_END; break commentenddashloop; // continue stateloop; default: state = MetaScanner.COMMENT; continue stateloop; } } // FALLTHRU DON'T REORDER case COMMENT_END: for (;;) { c = read(); switch (c) { case -1: break stateloop; case '>': state = MetaScanner.DATA; continue stateloop; case '-': continue; default: state = MetaScanner.COMMENT; continue stateloop; } } // XXX reorder point case COMMENT_START_DASH: c = read(); switch (c) { case -1: break stateloop; case '-': state = MetaScanner.COMMENT_END; continue stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: state = MetaScanner.COMMENT; continue stateloop; } // XXX reorder point case ATTRIBUTE_VALUE_SINGLE_QUOTED: for (;;) { if (reconsume) { reconsume = false; } else { c = read(); } switch (c) { case -1: break stateloop; case '\'': if (tryCharset()) { break stateloop; } state = MetaScanner.AFTER_ATTRIBUTE_VALUE_QUOTED; continue stateloop; default: if (metaState == A && (contentIndex == 6 || charsetIndex == 6)) { addToBuffer(c); } continue; } } // XXX reorder point case SCAN_UNTIL_GT: for (;;) { if (reconsume) { reconsume = false; } else { c = read(); } switch (c) { case -1: break stateloop; case '>': state = MetaScanner.DATA; continue stateloop; default: continue; } } } } stateSave = state; } /** * Adds a character to the accumulation buffer. * @param c the character to add */ private void addToBuffer(int c) { if (strBufLen == strBuf.length) { char[] newBuf = new char[strBuf.length + (strBuf.length << 1)]; System.arraycopy(strBuf, 0, newBuf, 0, strBuf.length); Portability.releaseArray(strBuf); strBuf = newBuf; } strBuf[strBufLen++] = (char)c; } /** * Attempts to extract a charset name from the accumulation buffer. * @return true if successful * @throws SAXException */ private boolean tryCharset() throws SAXException { if (metaState != A || !(contentIndex == 6 || charsetIndex == 6)) { return false; } String attVal = Portability.newStringFromBuffer(strBuf, 0, strBufLen); String candidateEncoding; if (contentIndex == 6) { candidateEncoding = TreeBuilder.extractCharsetFromContent(attVal); Portability.releaseString(attVal); } else { candidateEncoding = attVal; } if (candidateEncoding == null) { return false; } boolean success = tryCharset(candidateEncoding); Portability.releaseString(candidateEncoding); contentIndex = -1; charsetIndex = -1; return success; } /** * Tries to switch to an encoding. * * @param encoding * @return true if successful * @throws SAXException */ protected abstract boolean tryCharset(String encoding) throws SAXException; }