001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.commons.compress.archivers.zip; 020 021import java.lang.reflect.Constructor; 022import java.util.ArrayList; 023import java.util.List; 024import java.util.Objects; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.ConcurrentMap; 027import java.util.function.Supplier; 028import java.util.zip.ZipException; 029 030/** 031 * {@link ZipExtraField} related methods. 032 */ 033// CheckStyle:HideUtilityClassConstructorCheck OFF (bc) 034public class ExtraFieldUtils { 035 036 /** 037 * "enum" for the possible actions to take if the extra field cannot be parsed. 038 * <p> 039 * This class has been created long before Java 5 and would have been a real enum ever since. 040 * </p> 041 * 042 * @since 1.1 043 */ 044 public static final class UnparseableExtraField implements UnparseableExtraFieldBehavior { 045 046 /** 047 * Key for "throw an exception" action. 048 */ 049 public static final int THROW_KEY = 0; 050 /** 051 * Key for "skip" action. 052 */ 053 public static final int SKIP_KEY = 1; 054 /** 055 * Key for "read" action. 056 */ 057 public static final int READ_KEY = 2; 058 059 /** 060 * Throw an exception if field cannot be parsed. 061 */ 062 public static final UnparseableExtraField THROW = new UnparseableExtraField(THROW_KEY); 063 064 /** 065 * Skip the extra field entirely and don't make its data available - effectively removing the extra field data. 066 */ 067 public static final UnparseableExtraField SKIP = new UnparseableExtraField(SKIP_KEY); 068 069 /** 070 * Reads the extra field data into an instance of {@link UnparseableExtraFieldData UnparseableExtraFieldData}. 071 */ 072 public static final UnparseableExtraField READ = new UnparseableExtraField(READ_KEY); 073 074 private final int key; 075 076 private UnparseableExtraField(final int k) { 077 key = k; 078 } 079 080 /** 081 * Key of the action to take. 082 * 083 * @return the key 084 */ 085 public int getKey() { 086 return key; 087 } 088 089 @Override 090 public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local, final int claimedLength) 091 throws ZipException { 092 switch (key) { 093 case THROW_KEY: 094 throw new ZipException("Bad extra field starting at " + off + ". Block length of " + claimedLength + " bytes exceeds remaining data of " 095 + (len - WORD) + " bytes."); 096 case READ_KEY: 097 final UnparseableExtraFieldData field = new UnparseableExtraFieldData(); 098 if (local) { 099 field.parseFromLocalFileData(data, off, len); 100 } else { 101 field.parseFromCentralDirectoryData(data, off, len); 102 } 103 return field; 104 case SKIP_KEY: 105 return null; 106 default: 107 throw new ZipException("Unknown UnparseableExtraField key: " + key); 108 } 109 } 110 111 } 112 113 private static final int WORD = 4; 114 115 /** 116 * Static registry of known extra fields. 117 */ 118 private static final ConcurrentMap<ZipShort, Supplier<ZipExtraField>> IMPLEMENTATIONS; 119 120 static { 121 IMPLEMENTATIONS = new ConcurrentHashMap<>(); 122 IMPLEMENTATIONS.put(AsiExtraField.HEADER_ID, AsiExtraField::new); 123 IMPLEMENTATIONS.put(X5455_ExtendedTimestamp.HEADER_ID, X5455_ExtendedTimestamp::new); 124 IMPLEMENTATIONS.put(X7875_NewUnix.HEADER_ID, X7875_NewUnix::new); 125 IMPLEMENTATIONS.put(JarMarker.ID, JarMarker::new); 126 IMPLEMENTATIONS.put(UnicodePathExtraField.UPATH_ID, UnicodePathExtraField::new); 127 IMPLEMENTATIONS.put(UnicodeCommentExtraField.UCOM_ID, UnicodeCommentExtraField::new); 128 IMPLEMENTATIONS.put(Zip64ExtendedInformationExtraField.HEADER_ID, Zip64ExtendedInformationExtraField::new); 129 IMPLEMENTATIONS.put(X000A_NTFS.HEADER_ID, X000A_NTFS::new); 130 IMPLEMENTATIONS.put(X0014_X509Certificates.HEADER_ID, X0014_X509Certificates::new); 131 IMPLEMENTATIONS.put(X0015_CertificateIdForFile.HEADER_ID, X0015_CertificateIdForFile::new); 132 IMPLEMENTATIONS.put(X0016_CertificateIdForCentralDirectory.HEADER_ID, X0016_CertificateIdForCentralDirectory::new); 133 IMPLEMENTATIONS.put(X0017_StrongEncryptionHeader.HEADER_ID, X0017_StrongEncryptionHeader::new); 134 IMPLEMENTATIONS.put(X0019_EncryptionRecipientCertificateList.HEADER_ID, X0019_EncryptionRecipientCertificateList::new); 135 IMPLEMENTATIONS.put(ResourceAlignmentExtraField.ID, ResourceAlignmentExtraField::new); 136 } 137 138 static final ZipExtraField[] EMPTY_ZIP_EXTRA_FIELD_ARRAY = {}; 139 140 /** 141 * Creates an instance of the appropriate ExtraField, falls back to {@link UnrecognizedExtraField UnrecognizedExtraField}. 142 * 143 * @param headerId the header identifier 144 * @return an instance of the appropriate ExtraField 145 */ 146 public static ZipExtraField createExtraField(final ZipShort headerId) { 147 final ZipExtraField field = createExtraFieldNoDefault(headerId); 148 if (field != null) { 149 return field; 150 } 151 final UnrecognizedExtraField u = new UnrecognizedExtraField(); 152 u.setHeaderId(headerId); 153 return u; 154 } 155 156 /** 157 * Creates an instance of the appropriate {@link ZipExtraField}. 158 * 159 * @param headerId the header identifier 160 * @return an instance of the appropriate {@link ZipExtraField} or null if the id is not supported 161 * @since 1.19 162 */ 163 public static ZipExtraField createExtraFieldNoDefault(final ZipShort headerId) { 164 final Supplier<ZipExtraField> provider = IMPLEMENTATIONS.get(headerId); 165 return provider != null ? provider.get() : null; 166 } 167 168 /** 169 * Fills in the extra field data into the given instance. 170 * 171 * <p> 172 * Calls {@link ZipExtraField#parseFromCentralDirectoryData} or {@link ZipExtraField#parseFromLocalFileData} internally and wraps any 173 * {@link ArrayIndexOutOfBoundsException} thrown into a {@link ZipException}. 174 * </p> 175 * 176 * @param ze the extra field instance to fill 177 * @param data the array of extra field data 178 * @param off offset into data where this field's data starts 179 * @param len the length of this field's data 180 * @param local whether the extra field data stems from the local file header. If this is false then the data is part if the central directory header extra 181 * data. 182 * @return the filled field, will never be {@code null} 183 * @throws ZipException if an error occurs 184 * @since 1.19 185 */ 186 public static ZipExtraField fillExtraField(final ZipExtraField ze, final byte[] data, final int off, final int len, final boolean local) 187 throws ZipException { 188 try { 189 if (local) { 190 ze.parseFromLocalFileData(data, off, len); 191 } else { 192 ze.parseFromCentralDirectoryData(data, off, len); 193 } 194 return ze; 195 } catch (final ArrayIndexOutOfBoundsException e) { 196 throw ZipUtil.newZipException("Failed to parse corrupt ZIP extra field of type " + Integer.toHexString(ze.getHeaderId().getValue()), e); 197 } 198 } 199 200 /** 201 * Merges the central directory fields of the given ZipExtraFields. 202 * 203 * @param data an array of ExtraFields 204 * @return an array of bytes 205 */ 206 public static byte[] mergeCentralDirectoryData(final ZipExtraField[] data) { 207 final int dataLength = data.length; 208 final boolean lastIsUnparseableHolder = dataLength > 0 && data[dataLength - 1] instanceof UnparseableExtraFieldData; 209 final int regularExtraFieldCount = lastIsUnparseableHolder ? dataLength - 1 : dataLength; 210 211 int sum = WORD * regularExtraFieldCount; 212 for (final ZipExtraField element : data) { 213 sum += element.getCentralDirectoryLength().getValue(); 214 } 215 final byte[] result = new byte[sum]; 216 int start = 0; 217 for (int i = 0; i < regularExtraFieldCount; i++) { 218 System.arraycopy(data[i].getHeaderId().getBytes(), 0, result, start, 2); 219 System.arraycopy(data[i].getCentralDirectoryLength().getBytes(), 0, result, start + 2, 2); 220 start += WORD; 221 final byte[] central = data[i].getCentralDirectoryData(); 222 if (central != null) { 223 System.arraycopy(central, 0, result, start, central.length); 224 start += central.length; 225 } 226 } 227 if (lastIsUnparseableHolder) { 228 final byte[] central = data[dataLength - 1].getCentralDirectoryData(); 229 if (central != null) { 230 System.arraycopy(central, 0, result, start, central.length); 231 } 232 } 233 return result; 234 } 235 236 /** 237 * Merges the local file data fields of the given ZipExtraFields. 238 * 239 * @param data an array of ExtraFiles 240 * @return an array of bytes 241 */ 242 public static byte[] mergeLocalFileDataData(final ZipExtraField[] data) { 243 final int dataLength = data.length; 244 final boolean lastIsUnparseableHolder = dataLength > 0 && data[dataLength - 1] instanceof UnparseableExtraFieldData; 245 final int regularExtraFieldCount = lastIsUnparseableHolder ? dataLength - 1 : dataLength; 246 247 int sum = WORD * regularExtraFieldCount; 248 for (final ZipExtraField element : data) { 249 sum += element.getLocalFileDataLength().getValue(); 250 } 251 252 final byte[] result = new byte[sum]; 253 int start = 0; 254 for (int i = 0; i < regularExtraFieldCount; i++) { 255 System.arraycopy(data[i].getHeaderId().getBytes(), 0, result, start, 2); 256 System.arraycopy(data[i].getLocalFileDataLength().getBytes(), 0, result, start + 2, 2); 257 start += WORD; 258 final byte[] local = data[i].getLocalFileDataData(); 259 if (local != null) { 260 System.arraycopy(local, 0, result, start, local.length); 261 start += local.length; 262 } 263 } 264 if (lastIsUnparseableHolder) { 265 final byte[] local = data[dataLength - 1].getLocalFileDataData(); 266 if (local != null) { 267 System.arraycopy(local, 0, result, start, local.length); 268 } 269 } 270 return result; 271 } 272 273 /** 274 * Parses the array into ExtraFields and populate them with the given data as local file data, throwing an exception if the data cannot be parsed. 275 * 276 * @param data an array of bytes as it appears in local file data 277 * @return an array of ExtraFields 278 * @throws ZipException on error 279 */ 280 public static ZipExtraField[] parse(final byte[] data) throws ZipException { 281 return parse(data, true, UnparseableExtraField.THROW); 282 } 283 284 /** 285 * Parses the array into ExtraFields and populate them with the given data, throwing an exception if the data cannot be parsed. 286 * 287 * @param data an array of bytes 288 * @param local whether data originates from the local file data or the central directory 289 * @return an array of ExtraFields 290 * @throws ZipException on error 291 */ 292 public static ZipExtraField[] parse(final byte[] data, final boolean local) throws ZipException { 293 return parse(data, local, UnparseableExtraField.THROW); 294 } 295 296 /** 297 * Parses the array into ExtraFields and populate them with the given data. 298 * 299 * @param data an array of bytes 300 * @param parsingBehavior controls parsing of extra fields. 301 * @param local whether data originates from the local file data or the central directory 302 * @return an array of ExtraFields 303 * @throws ZipException on error 304 * @since 1.19 305 */ 306 public static ZipExtraField[] parse(final byte[] data, final boolean local, final ExtraFieldParsingBehavior parsingBehavior) throws ZipException { 307 final List<ZipExtraField> v = new ArrayList<>(); 308 int start = 0; 309 final int dataLength = data.length; 310 LOOP: while (start <= dataLength - WORD) { 311 final ZipShort headerId = new ZipShort(data, start); 312 final int length = new ZipShort(data, start + 2).getValue(); 313 if (start + WORD + length > dataLength) { 314 final ZipExtraField field = parsingBehavior.onUnparseableExtraField(data, start, dataLength - start, local, length); 315 if (field != null) { 316 v.add(field); 317 } 318 // since we cannot parse the data we must assume 319 // the extra field consumes the whole rest of the 320 // available data 321 break LOOP; 322 } 323 try { 324 final ZipExtraField ze = Objects.requireNonNull(parsingBehavior.createExtraField(headerId), "createExtraField must not return null"); 325 v.add(Objects.requireNonNull(parsingBehavior.fill(ze, data, start + WORD, length, local), "fill must not return null")); 326 start += length + WORD; 327 } catch (final InstantiationException | IllegalAccessException e) { 328 throw ZipUtil.newZipException(e.getMessage(), e); 329 } 330 } 331 332 return v.toArray(EMPTY_ZIP_EXTRA_FIELD_ARRAY); 333 } 334 335 /** 336 * Parses the array into ExtraFields and populate them with the given data. 337 * 338 * @param data an array of bytes 339 * @param local whether data originates from the local file data or the central directory 340 * @param onUnparseableData what to do if the extra field data cannot be parsed. 341 * @return an array of ExtraFields 342 * @throws ZipException on error 343 * @since 1.1 344 */ 345 public static ZipExtraField[] parse(final byte[] data, final boolean local, final UnparseableExtraField onUnparseableData) throws ZipException { 346 return parse(data, local, new ExtraFieldParsingBehavior() { 347 348 @Override 349 public ZipExtraField createExtraField(final ZipShort headerId) { 350 return ExtraFieldUtils.createExtraField(headerId); 351 } 352 353 @Override 354 public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) throws ZipException { 355 return fillExtraField(field, data, off, len, local); 356 } 357 358 @Override 359 public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local, final int claimedLength) 360 throws ZipException { 361 return onUnparseableData.onUnparseableExtraField(data, off, len, local, claimedLength); 362 } 363 }); 364 } 365 366 /** 367 * Registers a ZipExtraField implementation, overriding a matching existing entry. 368 * <p> 369 * The given class must have a no-arg constructor and implement the {@link ZipExtraField ZipExtraField interface}. 370 * </p> 371 * 372 * @param clazz the class to register. 373 * @deprecated Use {@link ZipArchiveInputStream#setExtraFieldSupport} instead 374 * to not leak instances between archives and applications. 375 */ 376 @Deprecated // note: when dropping update registration to move to a HashMap (static init) 377 public static void register(final Class<?> clazz) { 378 try { 379 final Constructor<? extends ZipExtraField> constructor = clazz.asSubclass(ZipExtraField.class).getConstructor(); 380 final ZipExtraField zef = clazz.asSubclass(ZipExtraField.class).getConstructor().newInstance(); 381 IMPLEMENTATIONS.put(zef.getHeaderId(), () -> { 382 try { 383 return constructor.newInstance(); 384 } catch (final ReflectiveOperationException e) { 385 throw new IllegalStateException(clazz.toString(), e); 386 } 387 }); 388 } catch (final ReflectiveOperationException e) { 389 throw new IllegalArgumentException(clazz.toString(), e); 390 } 391 } 392}