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.compress.archivers.zip; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Map; 022import java.util.Objects; 023import java.util.concurrent.ConcurrentHashMap; 024import java.util.zip.ZipException; 025 026/** 027 * {@link ZipExtraField} related methods. 028 * 029 * @NotThreadSafe because the HashMap is not synchronized. 030 */ 031// CheckStyle:HideUtilityClassConstructorCheck OFF (bc) 032public class ExtraFieldUtils { 033 034 /** 035 * "enum" for the possible actions to take if the extra field 036 * cannot be parsed. 037 * 038 * <p>This class has been created long before Java 5 and would 039 * have been a real enum ever since.</p> 040 * 041 * @since 1.1 042 */ 043 public static final class UnparseableExtraField implements UnparseableExtraFieldBehavior { 044 /** 045 * Key for "throw an exception" action. 046 */ 047 public static final int THROW_KEY = 0; 048 /** 049 * Key for "skip" action. 050 */ 051 public static final int SKIP_KEY = 1; 052 /** 053 * Key for "read" action. 054 */ 055 public static final int READ_KEY = 2; 056 057 /** 058 * Throw an exception if field cannot be parsed. 059 */ 060 public static final UnparseableExtraField THROW 061 = new UnparseableExtraField(THROW_KEY); 062 063 /** 064 * Skip the extra field entirely and don't make its data 065 * available - effectively removing the extra field data. 066 */ 067 public static final UnparseableExtraField SKIP 068 = new UnparseableExtraField(SKIP_KEY); 069 070 /** 071 * Read the extra field data into an instance of {@link 072 * UnparseableExtraFieldData UnparseableExtraFieldData}. 073 */ 074 public static final UnparseableExtraField READ 075 = new UnparseableExtraField(READ_KEY); 076 077 private final int key; 078 079 private UnparseableExtraField(final int k) { 080 key = k; 081 } 082 083 /** 084 * Key of the action to take. 085 * @return the key 086 */ 087 public int getKey() { return key; } 088 089 @Override 090 public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local, 091 final int claimedLength) throws ZipException { 092 switch(key) { 093 case THROW_KEY: 094 throw new ZipException("Bad extra field starting at " 095 + off + ". Block length of " 096 + claimedLength + " bytes exceeds remaining" 097 + " data of " 098 + (len - WORD) 099 + " bytes."); 100 case READ_KEY: 101 final UnparseableExtraFieldData field = new UnparseableExtraFieldData(); 102 if (local) { 103 field.parseFromLocalFileData(data, off, len); 104 } else { 105 field.parseFromCentralDirectoryData(data, off, len); 106 } 107 return field; 108 case SKIP_KEY: 109 return null; 110 default: 111 throw new ZipException("Unknown UnparseableExtraField key: " + key); 112 } 113 } 114 115 } 116 117 private static final int WORD = 4; 118 119 /** 120 * Static registry of known extra fields. 121 */ 122 private static final Map<ZipShort, Class<?>> IMPLEMENTATIONS; 123 124 static { 125 IMPLEMENTATIONS = new ConcurrentHashMap<>(); 126 register(AsiExtraField.class); 127 register(X5455_ExtendedTimestamp.class); 128 register(X7875_NewUnix.class); 129 register(JarMarker.class); 130 register(UnicodePathExtraField.class); 131 register(UnicodeCommentExtraField.class); 132 register(Zip64ExtendedInformationExtraField.class); 133 register(X000A_NTFS.class); 134 register(X0014_X509Certificates.class); 135 register(X0015_CertificateIdForFile.class); 136 register(X0016_CertificateIdForCentralDirectory.class); 137 register(X0017_StrongEncryptionHeader.class); 138 register(X0019_EncryptionRecipientCertificateList.class); 139 register(ResourceAlignmentExtraField.class); 140 } 141 142 static final ZipExtraField[] EMPTY_ZIP_EXTRA_FIELD_ARRAY = {}; 143 144 /** 145 * Create an instance of the appropriate ExtraField, falls back to 146 * {@link UnrecognizedExtraField UnrecognizedExtraField}. 147 * @param headerId the header identifier 148 * @return an instance of the appropriate ExtraField 149 * @throws InstantiationException if unable to instantiate the class 150 * @throws IllegalAccessException if not allowed to instantiate the class 151 */ 152 public static ZipExtraField createExtraField(final ZipShort headerId) 153 throws InstantiationException, IllegalAccessException { 154 final ZipExtraField field = createExtraFieldNoDefault(headerId); 155 if (field != null) { 156 return field; 157 } 158 final UnrecognizedExtraField u = new UnrecognizedExtraField(); 159 u.setHeaderId(headerId); 160 return u; 161 } 162 163 /** 164 * Create an instance of the appropriate ExtraField. 165 * @param headerId the header identifier 166 * @return an instance of the appropriate ExtraField or null if 167 * the id is not supported 168 * @throws InstantiationException if unable to instantiate the class 169 * @throws IllegalAccessException if not allowed to instantiate the class 170 * @since 1.19 171 */ 172 public static ZipExtraField createExtraFieldNoDefault(final ZipShort headerId) 173 throws InstantiationException, IllegalAccessException { 174 final Class<?> c = IMPLEMENTATIONS.get(headerId); 175 if (c != null) { 176 return (ZipExtraField) c.newInstance(); 177 } 178 return null; 179 } 180 181 /** 182 * Fills in the extra field data into the given instance. 183 * 184 * <p>Calls {@link ZipExtraField#parseFromCentralDirectoryData} or {@link ZipExtraField#parseFromLocalFileData} internally and wraps any {@link ArrayIndexOutOfBoundsException} thrown into a {@link ZipException}.</p> 185 * 186 * @param ze the extra field instance to fill 187 * @param data the array of extra field data 188 * @param off offset into data where this field's data starts 189 * @param len the length of this field's data 190 * @param local whether the extra field data stems from the local 191 * file header. If this is false then the data is part if the 192 * central directory header extra data. 193 * @return the filled field, will never be {@code null} 194 * @throws ZipException if an error occurs 195 * 196 * @since 1.19 197 */ 198 public static ZipExtraField fillExtraField(final ZipExtraField ze, final byte[] data, final int off, 199 final int len, final boolean local) throws ZipException { 200 try { 201 if (local) { 202 ze.parseFromLocalFileData(data, off, len); 203 } else { 204 ze.parseFromCentralDirectoryData(data, off, len); 205 } 206 return ze; 207 } catch (final ArrayIndexOutOfBoundsException aiobe) { 208 throw (ZipException) new ZipException("Failed to parse corrupt ZIP extra field of type " 209 + Integer.toHexString(ze.getHeaderId().getValue())).initCause(aiobe); 210 } 211 } 212 213 /** 214 * Merges the central directory fields of the given ZipExtraFields. 215 * @param data an array of ExtraFields 216 * @return an array of bytes 217 */ 218 public static byte[] mergeCentralDirectoryData(final ZipExtraField[] data) { 219 final int dataLength = data.length; 220 final boolean lastIsUnparseableHolder = dataLength > 0 221 && data[dataLength - 1] instanceof UnparseableExtraFieldData; 222 final int regularExtraFieldCount = 223 lastIsUnparseableHolder ? dataLength - 1 : dataLength; 224 225 int sum = WORD * regularExtraFieldCount; 226 for (final ZipExtraField element : data) { 227 sum += element.getCentralDirectoryLength().getValue(); 228 } 229 final byte[] result = new byte[sum]; 230 int start = 0; 231 for (int i = 0; i < regularExtraFieldCount; i++) { 232 System.arraycopy(data[i].getHeaderId().getBytes(), 233 0, result, start, 2); 234 System.arraycopy(data[i].getCentralDirectoryLength().getBytes(), 235 0, result, start + 2, 2); 236 start += WORD; 237 final byte[] central = data[i].getCentralDirectoryData(); 238 if (central != null) { 239 System.arraycopy(central, 0, result, start, central.length); 240 start += central.length; 241 } 242 } 243 if (lastIsUnparseableHolder) { 244 final byte[] central = data[dataLength - 1].getCentralDirectoryData(); 245 if (central != null) { 246 System.arraycopy(central, 0, result, start, central.length); 247 } 248 } 249 return result; 250 } 251 252 /** 253 * Merges the local file data fields of the given ZipExtraFields. 254 * @param data an array of ExtraFiles 255 * @return an array of bytes 256 */ 257 public static byte[] mergeLocalFileDataData(final ZipExtraField[] data) { 258 final int dataLength = data.length; 259 final boolean lastIsUnparseableHolder = dataLength > 0 260 && data[dataLength - 1] instanceof UnparseableExtraFieldData; 261 final int regularExtraFieldCount = 262 lastIsUnparseableHolder ? dataLength - 1 : dataLength; 263 264 int sum = WORD * regularExtraFieldCount; 265 for (final ZipExtraField element : data) { 266 sum += element.getLocalFileDataLength().getValue(); 267 } 268 269 final byte[] result = new byte[sum]; 270 int start = 0; 271 for (int i = 0; i < regularExtraFieldCount; i++) { 272 System.arraycopy(data[i].getHeaderId().getBytes(), 273 0, result, start, 2); 274 System.arraycopy(data[i].getLocalFileDataLength().getBytes(), 275 0, result, start + 2, 2); 276 start += WORD; 277 final byte[] local = data[i].getLocalFileDataData(); 278 if (local != null) { 279 System.arraycopy(local, 0, result, start, local.length); 280 start += local.length; 281 } 282 } 283 if (lastIsUnparseableHolder) { 284 final byte[] local = data[dataLength - 1].getLocalFileDataData(); 285 if (local != null) { 286 System.arraycopy(local, 0, result, start, local.length); 287 } 288 } 289 return result; 290 } 291 292 /** 293 * Split the array into ExtraFields and populate them with the 294 * given data as local file data, throwing an exception if the 295 * data cannot be parsed. 296 * @param data an array of bytes as it appears in local file data 297 * @return an array of ExtraFields 298 * @throws ZipException on error 299 */ 300 public static ZipExtraField[] parse(final byte[] data) throws ZipException { 301 return parse(data, true, UnparseableExtraField.THROW); 302 } 303 304 /** 305 * Split the array into ExtraFields and populate them with the 306 * given data, throwing an exception if the data cannot be parsed. 307 * @param data an array of bytes 308 * @param local whether data originates from the local file data 309 * or the central directory 310 * @return an array of ExtraFields 311 * @throws ZipException on error 312 */ 313 public static ZipExtraField[] parse(final byte[] data, final boolean local) 314 throws ZipException { 315 return parse(data, local, UnparseableExtraField.THROW); 316 } 317 318 /** 319 * Split the array into ExtraFields and populate them with the 320 * given data. 321 * @param data an array of bytes 322 * @param parsingBehavior controls parsing of extra fields. 323 * @param local whether data originates from the local file data 324 * or the central directory 325 * @return an array of ExtraFields 326 * @throws ZipException on error 327 * 328 * @since 1.19 329 */ 330 public static ZipExtraField[] parse(final byte[] data, final boolean local, 331 final ExtraFieldParsingBehavior parsingBehavior) 332 throws ZipException { 333 final List<ZipExtraField> v = new ArrayList<>(); 334 int start = 0; 335 final int dataLength = data.length; 336 LOOP: 337 while (start <= dataLength - WORD) { 338 final ZipShort headerId = new ZipShort(data, start); 339 final int length = new ZipShort(data, start + 2).getValue(); 340 if (start + WORD + length > dataLength) { 341 final ZipExtraField field = parsingBehavior.onUnparseableExtraField(data, start, dataLength - start, 342 local, length); 343 if (field != null) { 344 v.add(field); 345 } 346 // since we cannot parse the data we must assume 347 // the extra field consumes the whole rest of the 348 // available data 349 break LOOP; 350 } 351 try { 352 final ZipExtraField ze = Objects.requireNonNull(parsingBehavior.createExtraField(headerId), 353 "createExtraField must not return null"); 354 v.add(Objects.requireNonNull(parsingBehavior.fill(ze, data, start + WORD, length, local), 355 "fill must not return null")); 356 start += length + WORD; 357 } catch (final InstantiationException | IllegalAccessException ie) { 358 throw (ZipException) new ZipException(ie.getMessage()).initCause(ie); 359 } 360 } 361 362 return v.toArray(EMPTY_ZIP_EXTRA_FIELD_ARRAY); 363 } 364 365 /** 366 * Split the array into ExtraFields and populate them with the 367 * given data. 368 * @param data an array of bytes 369 * @param local whether data originates from the local file data 370 * or the central directory 371 * @param onUnparseableData what to do if the extra field data 372 * cannot be parsed. 373 * @return an array of ExtraFields 374 * @throws ZipException on error 375 * 376 * @since 1.1 377 */ 378 public static ZipExtraField[] parse(final byte[] data, final boolean local, 379 final UnparseableExtraField onUnparseableData) 380 throws ZipException { 381 return parse(data, local, new ExtraFieldParsingBehavior() { 382 @Override 383 public ZipExtraField createExtraField(final ZipShort headerId) 384 throws ZipException, InstantiationException, IllegalAccessException { 385 return ExtraFieldUtils.createExtraField(headerId); 386 } 387 388 @Override 389 public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) 390 throws ZipException { 391 return fillExtraField(field, data, off, len, local); 392 } 393 394 @Override 395 public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local, 396 final int claimedLength) throws ZipException { 397 return onUnparseableData.onUnparseableExtraField(data, off, len, local, claimedLength); 398 } 399 }); 400 } 401 402 /** 403 * Register a ZipExtraField implementation. 404 * 405 * <p>The given class must have a no-arg constructor and implement 406 * the {@link ZipExtraField ZipExtraField interface}.</p> 407 * @param c the class to register 408 */ 409 public static void register(final Class<?> c) { 410 try { 411 final ZipExtraField ze = (ZipExtraField) c.getConstructor().newInstance(); 412 IMPLEMENTATIONS.put(ze.getHeaderId(), c); 413 } catch (final ClassCastException cc) { // NOSONAR 414 throw new IllegalArgumentException(c + " doesn't implement ZipExtraField"); //NOSONAR 415 } catch (final InstantiationException ie) { // NOSONAR 416 throw new IllegalArgumentException(c + " is not a concrete class"); //NOSONAR 417 } catch (final IllegalAccessException ie) { // NOSONAR 418 throw new IllegalArgumentException(c + "'s no-arg constructor is not public"); //NOSONAR 419 } catch (ReflectiveOperationException e) { 420 throw new IllegalArgumentException(c + ": " + e); //NOSONAR 421 } 422 } 423}