001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.common; 018 019import java.io.ByteArrayInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.PushbackInputStream; 024import java.util.Map; 025 026import org.apache.commons.imaging.ImageReadException; 027 028/** 029 * A rudimentary preprocessor and parser for the C programming 030 * language. 031 * 032 * FIXME replace this by a parser generated via ANTLR (if we really need it?!) 033 */ 034public class BasicCParser { 035 private final PushbackInputStream is; 036 037 public BasicCParser(final ByteArrayInputStream is) { 038 this.is = new PushbackInputStream(is); 039 } 040 041 public String nextToken() throws IOException, ImageReadException { 042 // I don't know how complete the C parsing in an XPM file 043 // is meant to be, this is just the very basics... 044 045 boolean inString = false; 046 boolean inIdentifier = false; 047 boolean hadBackSlash = false; 048 final StringBuilder token = new StringBuilder(); 049 for (int c = is.read(); c != -1; c = is.read()) { 050 if (inString) { 051 if (c == '\\') { 052 token.append('\\'); 053 hadBackSlash = !hadBackSlash; 054 } else if (c == '"') { 055 token.append('"'); 056 if (!hadBackSlash) { 057 return token.toString(); 058 } 059 hadBackSlash = false; 060 } else if (c == '\r' || c == '\n') { 061 throw new ImageReadException( 062 "Unterminated string in XPM file"); 063 } else { 064 token.append((char) c); 065 hadBackSlash = false; 066 } 067 } else if (inIdentifier) { 068 if (Character.isLetterOrDigit(c) || c == '_') { 069 token.append((char) c); 070 } else { 071 is.unread(c); 072 return token.toString(); 073 } 074 } else { 075 if (c == '"') { 076 token.append('"'); 077 inString = true; 078 } else if (Character.isLetterOrDigit(c) || c == '_') { 079 token.append((char) c); 080 inIdentifier = true; 081 } else if (c == '{' || c == '}' || c == '[' || c == ']' 082 || c == '*' || c == ';' || c == '=' || c == ',') { 083 token.append((char) c); 084 return token.toString(); 085 } else if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { 086 // ignore 087 } else { 088 throw new ImageReadException( 089 "Unhandled/invalid character '" + ((char) c) 090 + "' found in XPM file"); 091 } 092 } 093 } 094 095 if (inIdentifier) { 096 return token.toString(); 097 } 098 if (inString) { 099 throw new ImageReadException("Unterminated string ends XMP file"); 100 } 101 return null; 102 } 103 104 public static ByteArrayOutputStream preprocess(final InputStream is, 105 final StringBuilder firstComment, final Map<String, String> defines) 106 throws IOException, ImageReadException { 107 boolean inSingleQuotes = false; 108 boolean inString = false; 109 boolean inComment = false; 110 boolean inDirective = false; 111 boolean hadSlash = false; 112 boolean hadStar = false; 113 boolean hadBackSlash = false; 114 final ByteArrayOutputStream out = new ByteArrayOutputStream(); 115 boolean seenFirstComment = (firstComment == null); 116 final StringBuilder directiveBuffer = new StringBuilder(); 117 for (int c = is.read(); c != -1; c = is.read()) { 118 if (inComment) { 119 if (c == '*') { 120 if (hadStar && !seenFirstComment) { 121 firstComment.append('*'); 122 } 123 hadStar = true; 124 } else if (c == '/') { 125 if (hadStar) { 126 hadStar = false; 127 inComment = false; 128 seenFirstComment = true; 129 } else { 130 if (!seenFirstComment) { 131 firstComment.append((char) c); 132 } 133 } 134 } else { 135 if (hadStar && !seenFirstComment) { 136 firstComment.append('*'); 137 } 138 hadStar = false; 139 if (!seenFirstComment) { 140 firstComment.append((char) c); 141 } 142 } 143 } else if (inSingleQuotes) { 144 if (c == '\\') { 145 if (hadBackSlash) { 146 out.write('\\'); 147 out.write('\\'); 148 hadBackSlash = false; 149 } else { 150 hadBackSlash = true; 151 } 152 } else if (c == '\'') { 153 if (hadBackSlash) { 154 out.write('\\'); 155 hadBackSlash = false; 156 } else { 157 inSingleQuotes = false; 158 } 159 out.write('\''); 160 } else if (c == '\r' || c == '\n') { 161 throw new ImageReadException("Unterminated single quote in file"); 162 } else { 163 if (hadBackSlash) { 164 out.write('\\'); 165 hadBackSlash = false; 166 } 167 out.write(c); 168 } 169 } else if (inString) { 170 if (c == '\\') { 171 if (hadBackSlash) { 172 out.write('\\'); 173 out.write('\\'); 174 hadBackSlash = false; 175 } else { 176 hadBackSlash = true; 177 } 178 } else if (c == '"') { 179 if (hadBackSlash) { 180 out.write('\\'); 181 hadBackSlash = false; 182 } else { 183 inString = false; 184 } 185 out.write('"'); 186 } else if (c == '\r' || c == '\n') { 187 throw new ImageReadException("Unterminated string in file"); 188 } else { 189 if (hadBackSlash) { 190 out.write('\\'); 191 hadBackSlash = false; 192 } 193 out.write(c); 194 } 195 } else if (inDirective) { 196 if (c == '\r' || c == '\n') { 197 inDirective = false; 198 final String[] tokens = tokenizeRow(directiveBuffer.toString()); 199 if (tokens.length < 2 || tokens.length > 3) { 200 throw new ImageReadException("Bad preprocessor directive"); 201 } 202 if (!tokens[0].equals("define")) { 203 throw new ImageReadException("Invalid/unsupported " 204 + "preprocessor directive '" + tokens[0] + "'"); 205 } 206 defines.put(tokens[1], (tokens.length == 3) ? tokens[2] 207 : null); 208 directiveBuffer.setLength(0); 209 } else { 210 directiveBuffer.append((char) c); 211 } 212 } else { 213 if (c == '/') { 214 if (hadSlash) { 215 out.write('/'); 216 } 217 hadSlash = true; 218 } else if (c == '*') { 219 if (hadSlash) { 220 inComment = true; 221 hadSlash = false; 222 } else { 223 out.write(c); 224 } 225 } else if (c == '\'') { 226 if (hadSlash) { 227 out.write('/'); 228 } 229 hadSlash = false; 230 out.write(c); 231 inSingleQuotes = true; 232 } else if (c == '"') { 233 if (hadSlash) { 234 out.write('/'); 235 } 236 hadSlash = false; 237 out.write(c); 238 inString = true; 239 } else if (c == '#') { 240 if (defines == null) { 241 throw new ImageReadException("Unexpected preprocessor directive"); 242 } 243 inDirective = true; 244 } else { 245 if (hadSlash) { 246 out.write('/'); 247 } 248 hadSlash = false; 249 out.write(c); 250 // Only whitespace allowed before first comment: 251 if (c != ' ' && c != '\t' && c != '\r' && c != '\n') { 252 seenFirstComment = true; 253 } 254 } 255 } 256 } 257 if (hadSlash) { 258 out.write('/'); 259 } 260 if (hadStar) { 261 out.write('*'); 262 } 263 if (inString) { 264 throw new ImageReadException("Unterminated string at the end of file"); 265 } 266 if (inComment) { 267 throw new ImageReadException("Unterminated comment at the end of file"); 268 } 269 return out; 270 } 271 272 public static String[] tokenizeRow(final String row) { 273 final String[] tokens = row.split("[ \t]"); 274 int numLiveTokens = 0; 275 for (final String token : tokens) { 276 if (token != null && token.length() > 0) { 277 ++numLiveTokens; 278 } 279 } 280 final String[] liveTokens = new String[numLiveTokens]; 281 int next = 0; 282 for (final String token : tokens) { 283 if (token != null && token.length() > 0) { 284 liveTokens[next++] = token; 285 } 286 } 287 return liveTokens; 288 } 289 290 public static void unescapeString(final StringBuilder stringBuilder, final String string) 291 throws ImageReadException { 292 if (string.length() < 2) { 293 throw new ImageReadException("Parsing XPM file failed, " 294 + "string is too short"); 295 } 296 if (string.charAt(0) != '"' 297 || string.charAt(string.length() - 1) != '"') { 298 throw new ImageReadException("Parsing XPM file failed, " 299 + "string not surrounded by '\"'"); 300 } 301 boolean hadBackSlash = false; 302 for (int i = 1; i < (string.length() - 1); i++) { 303 final char c = string.charAt(i); 304 if (hadBackSlash) { 305 if (c == '\\') { 306 stringBuilder.append('\\'); 307 } else if (c == '"') { 308 stringBuilder.append('"'); 309 } else if (c == '\'') { 310 stringBuilder.append('\''); 311 } else if (c == 'x') { 312 if (i + 2 >= string.length()) { 313 throw new ImageReadException( 314 "Parsing XPM file failed, " 315 + "hex constant in string too short"); 316 } 317 final char hex1 = string.charAt(i + 1); 318 final char hex2 = string.charAt(i + 2); 319 i += 2; 320 int constant; 321 try { 322 constant = Integer.parseInt(hex1 + Character.toString(hex2), 16); 323 } catch (final NumberFormatException nfe) { 324 throw new ImageReadException( 325 "Parsing XPM file failed, " 326 + "hex constant invalid", nfe); 327 } 328 stringBuilder.append((char) constant); 329 } else if (c == '0' || c == '1' || c == '2' || c == '3' 330 || c == '4' || c == '5' || c == '6' || c == '7') { 331 int length = 1; 332 if (i + 1 < string.length() && '0' <= string.charAt(i + 1) 333 && string.charAt(i + 1) <= '7') { 334 ++length; 335 } 336 if (i + 2 < string.length() && '0' <= string.charAt(i + 2) 337 && string.charAt(i + 2) <= '7') { 338 ++length; 339 } 340 int constant = 0; 341 for (int j = 0; j < length; j++) { 342 constant *= 8; 343 constant += (string.charAt(i + j) - '0'); 344 } 345 i += length - 1; 346 stringBuilder.append((char) constant); 347 } else if (c == 'a') { 348 stringBuilder.append((char) 0x07); 349 } else if (c == 'b') { 350 stringBuilder.append((char) 0x08); 351 } else if (c == 'f') { 352 stringBuilder.append((char) 0x0c); 353 } else if (c == 'n') { 354 stringBuilder.append((char) 0x0a); 355 } else if (c == 'r') { 356 stringBuilder.append((char) 0x0d); 357 } else if (c == 't') { 358 stringBuilder.append((char) 0x09); 359 } else if (c == 'v') { 360 stringBuilder.append((char) 0x0b); 361 } else { 362 throw new ImageReadException("Parsing XPM file failed, " 363 + "invalid escape sequence"); 364 } 365 hadBackSlash = false; 366 } else { 367 if (c == '\\') { 368 hadBackSlash = true; 369 } else if (c == '"') { 370 throw new ImageReadException("Parsing XPM file failed, " 371 + "extra '\"' found in string"); 372 } else { 373 stringBuilder.append(c); 374 } 375 } 376 } 377 if (hadBackSlash) { 378 throw new ImageReadException("Parsing XPM file failed, " 379 + "unterminated escape sequence found in string"); 380 } 381 } 382}