1 16 17 package de.schlichtherle.util.zip; 18 19 import de.schlichtherle.io.rof.BufferedReadOnlyFile; 20 import de.schlichtherle.io.rof.ReadOnlyFile; 21 import de.schlichtherle.io.rof.SimpleReadOnlyFile; 22 23 import java.io.*; 24 import java.lang.ref.*; 25 import java.util.*; 26 import java.util.zip.*; 27 28 46 public class BasicZipFile implements ZipConstants { 47 48 private static final long LONG_MSB = 0x8000000000000000L; 49 50 private static final int LFH_FILE_NAME_LENGTH_OFFSET = 51 4 + 52 2 + 53 2 + 54 2 + 55 2 + 56 2 + 57 4 + 58 4 + 59 4; 60 61 private static final int EOCD_NUM_ENTRIES_OFFSET = 62 4 + 63 2 + 64 + 65 2 + 66 + 67 2; 68 69 private static final int EOCD_CD_SIZE_OFFSET = 70 EOCD_MIN_LEN - 10; 71 72 private static final int EOCD_CD_LOCATION_OFFSET = 73 4 + 74 2 + 75 + 76 2 + 77 + 78 2 + 79 + 80 2 + 81 4; 82 83 private static final int EOCD_COMMENT_OFFSET = 84 EOCD_MIN_LEN - 2; 85 86 private static final Set allocatedInflaters = new HashSet(); 87 private static final List releasedInflaters = new LinkedList(); 88 89 92 private final String encoding; 93 94 97 private String comment; 98 99 102 private final Map entries = new HashMap(); 103 104 105 private ReadOnlyFile archive; 106 107 110 private int openStreams; 111 112 115 private long preamble; 116 117 120 private long postamble; 121 122 private OffsetMapper mapper; 123 124 135 public BasicZipFile(String name) 136 throws NullPointerException , 137 FileNotFoundException, 138 ZipException, 139 IOException { 140 this.encoding = DEFAULT_ENCODING; 141 try { 142 init(null, new File(name), true, false); 143 } catch (UnsupportedEncodingException cannotHappen) { 144 throw new AssertionError (cannotHappen); 145 } 146 } 147 148 163 public BasicZipFile(String name, String encoding) 164 throws NullPointerException , 165 UnsupportedEncodingException, 166 FileNotFoundException, 167 ZipException, 168 IOException { 169 this.encoding = encoding; 170 init(null, new File(name), true, false); 171 } 172 173 210 public BasicZipFile( 211 String name, 212 String encoding, 213 boolean preambled, 214 boolean postambled) 215 throws NullPointerException , 216 UnsupportedEncodingException, 217 FileNotFoundException, 218 ZipException, 219 IOException { 220 this.encoding = encoding; 221 init(null, new File(name), preambled, postambled); 222 } 223 224 235 public BasicZipFile(File file) 236 throws NullPointerException , 237 FileNotFoundException, 238 ZipException, 239 IOException { 240 this.encoding = DEFAULT_ENCODING; 241 try { 242 init(null, file, true, false); 243 } catch (UnsupportedEncodingException cannotHappen) { 244 throw new AssertionError (cannotHappen); 245 } 246 } 247 248 264 public BasicZipFile(File file, String encoding) 265 throws NullPointerException , 266 UnsupportedEncodingException, 267 FileNotFoundException, 268 ZipException, 269 IOException { 270 this.encoding = encoding; 271 init(null, file, true, false); 272 } 273 274 312 public BasicZipFile( 313 File file, 314 String encoding, 315 boolean preambled, 316 boolean postambled) 317 throws NullPointerException , 318 UnsupportedEncodingException, 319 FileNotFoundException, 320 ZipException, 321 IOException { 322 this.encoding = encoding; 323 init(null, file, preambled, postambled); 324 } 325 326 338 public BasicZipFile(ReadOnlyFile rof) 339 throws NullPointerException , 340 FileNotFoundException, 341 ZipException, 342 IOException { 343 this.encoding = DEFAULT_ENCODING; 344 try { 345 init(rof, null, true, false); 346 } catch (UnsupportedEncodingException cannotHappen) { 347 throw new AssertionError (cannotHappen); 348 } 349 } 350 351 368 public BasicZipFile(ReadOnlyFile rof, String encoding) 369 throws NullPointerException , 370 UnsupportedEncodingException, 371 FileNotFoundException, 372 ZipException, 373 IOException { 374 this.encoding = encoding; 375 init(rof, null, true, false); 376 } 377 378 417 public BasicZipFile( 418 ReadOnlyFile rof, 419 String encoding, 420 boolean preambled, 421 boolean postambled) 422 throws NullPointerException , 423 UnsupportedEncodingException, 424 FileNotFoundException, 425 ZipException, 426 IOException { 427 this.encoding = encoding; 428 init(rof, null, preambled, postambled); 429 } 430 431 private void init( 432 ReadOnlyFile rof, 433 final File file, 434 final boolean preambled, 435 final boolean postambled) 436 throws NullPointerException , 437 UnsupportedEncodingException, 438 FileNotFoundException, 439 ZipException, 440 IOException { 441 if (encoding == null) 443 throw new NullPointerException ("encoding"); 444 new String (new byte[0], encoding); if (rof == null) { 446 if (file == null) 447 throw new NullPointerException (); 448 rof = createReadOnlyFile(file); 449 } else { assert file == null; 451 } 452 archive = rof; 453 454 try { 455 final BufferedReadOnlyFile brof; 456 if (archive instanceof BufferedReadOnlyFile) 457 brof = (BufferedReadOnlyFile) archive; 458 else 459 brof = new BufferedReadOnlyFile(archive); 460 mountCentralDirectory(brof, preambled, postambled); 461 } catch (IOException failure) { 463 if (file != null) 464 rof.close(); 465 throw failure; 466 } 467 468 assert mapper != null; 469 } 470 471 480 protected ReadOnlyFile createReadOnlyFile(File file) 481 throws FileNotFoundException, IOException { 482 return new SimpleReadOnlyFile(file); 483 } 484 485 496 private void mountCentralDirectory( 497 final ReadOnlyFile rof, 498 final boolean preambled, 499 final boolean postambled) 500 throws ZipException, IOException { 501 int numEntries = findCentralDirectory(rof, preambled, postambled); 502 assert mapper != null; 503 504 preamble = Long.MAX_VALUE; 505 506 final byte[] sig = new byte[4]; 507 final byte[] cfh = new byte[CFH_MIN_LEN - sig.length]; 508 for (; ; numEntries--) { 509 rof.readFully(sig); 510 if (readUInt(sig) != CFH_SIG) 511 break; 512 513 rof.readFully(cfh); 514 final int entryNameLen = readUShort(cfh, 24); 515 final byte[] entryName = new byte[entryNameLen]; 516 rof.readFully(entryName); 517 518 final ZipEntry ze = createZipEntry(new String (entryName, encoding)); 519 try { 520 int off = 0; 521 522 final int versionMadeBy = readUShort(cfh, off); 523 off += 2; 524 ze.setPlatform((short) ((versionMadeBy >> 8) & 0xFF)); 525 526 off += 4; 528 ze.setMethod((short) readUShort(cfh, off)); 529 off += 2; 530 531 ze.setDosTime(readUInt(cfh, off)); 532 off += 4; 533 534 ze.setCrc(readUInt(cfh, off)); 535 off += 4; 536 537 ze.setCompressedSize(readUInt(cfh, off)); 538 off += 4; 539 540 ze.setSize(readUInt(cfh, off)); 541 off += 4; 542 543 off += 2; 545 final int extraLen = readUShort(cfh, off); 546 off += 2; 547 548 final int commentLen = readUShort(cfh, off); 549 off += 2; 550 551 off += 2; 553 off += 2; 555 556 off += 4; 558 559 final long lfhOff = mapper.location(readUInt(cfh, off)); 561 562 ze.offset = lfhOff | LONG_MSB; 566 567 if (lfhOff < preamble) 569 preamble = lfhOff; 570 571 entries.put(ze.getName(), ze); 572 573 if (extraLen > 0) { 574 final byte[] extra = new byte[extraLen]; 575 rof.readFully(extra); 576 ze.setExtra(extra); 577 } 578 579 if (commentLen > 0) { 580 final byte[] comment = new byte[commentLen]; 581 rof.readFully(comment); 582 ze.setComment(new String (comment, encoding)); 583 } 584 } catch (IllegalArgumentException incompatibleZipFile) { 585 final ZipException exc = new ZipException(ze.getName()); 586 exc.initCause(incompatibleZipFile); 587 throw exc; 588 } 589 } 590 591 if (numEntries % 65536 != 0) 600 throw new ZipException( 601 "Not a ZIP compatible file: Expected " + 602 Math.abs(numEntries) + 603 (numEntries > 0 ? " more" : " less") + 604 " entries in the Central Directory!"); 605 606 if (preamble == Long.MAX_VALUE) 607 preamble = 0; 608 } 609 610 623 private int findCentralDirectory( 624 final ReadOnlyFile rof, 625 boolean preambled, 626 final boolean postambled) 627 throws ZipException, IOException { 628 final byte[] sig = new byte[4]; 629 if (!preambled) { 630 rof.seek(0); 631 rof.readFully(sig); 632 final long signature = readUInt(sig); 633 preambled = signature == LFH_SIG || signature == EOCD_SIG; 636 } 637 if (preambled) { 638 final long length = rof.length(); 639 final long max = length - EOCD_MIN_LEN; 640 final long min = !postambled && max >= 0xFFFF ? max - 0xFFFF : 0; 641 for (long eocdOff = max; eocdOff >= min; eocdOff--) { 642 rof.seek(eocdOff); 643 rof.readFully(sig); 644 if (readUInt(sig) != EOCD_SIG) 645 continue; 646 647 final byte[] eocd = new byte[EOCD_MIN_LEN - sig.length]; 649 rof.readFully(eocd); 650 final int numEntries = readUShort(eocd, EOCD_NUM_ENTRIES_OFFSET - sig.length); 651 final long cdSize = readUInt(eocd, EOCD_CD_SIZE_OFFSET - sig.length); 652 final long cdLoc = readUInt(eocd, EOCD_CD_LOCATION_OFFSET - sig.length); 653 final int commentLen = readUShort(eocd, EOCD_COMMENT_OFFSET - sig.length); 654 if (commentLen > 0) { 655 final byte[] comment = new byte[commentLen]; 656 rof.readFully(comment); 657 setComment(new String (comment, encoding)); 658 } 659 postamble = length - rof.getFilePointer(); 660 661 long start = eocdOff - cdSize; 663 rof.seek(start); 664 start -= cdLoc; 665 if (start != 0) { 666 mapper = new IrregularOffsetMapper(start); 667 } else { 668 mapper = new OffsetMapper(); 669 } 670 671 return numEntries; 672 } 673 } 674 throw new ZipException( 675 "Not a ZIP compatible file: End Of Central Directory signature is missing!"); 676 } 677 678 681 protected ZipEntry createZipEntry(String name) { 682 return new ZipEntry(name); 683 } 684 685 689 public String getComment() { 690 return comment; 691 } 692 693 private void setComment(String comment) { 694 this.comment = comment; 695 } 696 697 701 public boolean busy() { 702 return openStreams > 0; 703 } 704 705 708 public String getEncoding() { 709 return encoding; 710 } 711 712 717 public Enumeration entries() { 718 return Collections.enumeration(entries.values()); 719 } 720 721 729 public ZipEntry getEntry(String name) { 730 return (ZipEntry) entries.get(name); 731 } 732 733 736 public int size() { 737 return entries.size(); 738 } 739 740 743 public long length() throws IOException { 744 return archive.length(); 745 } 746 747 755 public long getPreambleLength() { 756 return preamble; 757 } 758 759 775 public InputStream getPreambleInputStream() throws IOException { 776 ensureOpen(); 777 return new IntervalInputStream(0, preamble); 778 } 779 780 788 public long getPostambleLength() { 789 return postamble; 790 } 791 792 808 public InputStream getPostambleInputStream() throws IOException { 809 ensureOpen(); 810 return new IntervalInputStream(archive.length() - postamble, postamble); 811 } 812 813 820 public boolean offsetsConsiderPreamble() { 821 assert mapper != null; 822 return mapper.location(0) == 0; 823 } 824 825 831 public final InputStream getCheckedInputStream(final ZipEntry entry) 832 throws IOException { 833 return getInputStream(entry.getName(), true, true); 834 } 835 836 855 public final InputStream getCheckedInputStream(final String name) 856 throws IOException { 857 return getInputStream(name, true, true); 858 } 859 860 864 public final InputStream getInputStream(ZipEntry entry) 865 throws IOException { 866 return getInputStream(entry.getName(), false, true); 867 } 868 869 875 public final InputStream getInputStream(ZipEntry entry, boolean inflate) 876 throws IOException { 877 return getInputStream(entry.getName(), false, inflate); 878 } 879 880 884 public final InputStream getInputStream(String name) 885 throws IOException { 886 return getInputStream(name, false, true); 887 } 888 889 920 public InputStream getInputStream( 921 final String name, 922 final boolean inflate) 923 throws IOException { 924 return getInputStream(name, false, inflate); 925 } 926 927 977 protected InputStream getInputStream( 978 final String name, 979 final boolean check, 980 final boolean inflate) 981 throws ZipException, 982 IOException { 983 if (name == null) 984 throw new NullPointerException (); 985 986 ensureOpen(); 987 final ZipEntry entry = (ZipEntry) entries.get(name); 988 if (entry == null) 989 return null; 990 991 long offset = entry.offset; 992 assert offset != -1; 993 if (offset < 0) { 994 offset &= ~LONG_MSB; archive.seek(offset); 998 final byte[] lfh = new byte[LFH_MIN_LEN]; 999 archive.readFully(lfh); 1000 final long lfhSig = readUInt(lfh); 1001 if (lfhSig != LFH_SIG) 1002 throw new ZipException(name + ": Local File Header signature expected!"); 1003 offset += LFH_MIN_LEN 1004 + readUShort(lfh, LFH_FILE_NAME_LENGTH_OFFSET) + readUShort(lfh, LFH_FILE_NAME_LENGTH_OFFSET + 2); entry.offset = offset; 1007 } 1008 1009 final IntervalInputStream iis 1010 = new IntervalInputStream(offset, entry.getCompressedSize()); 1011 final int bufSize = getBufferSize(entry); 1012 InputStream in; 1013 switch (entry.getMethod()) { 1014 case ZipEntry.DEFLATED: 1015 if (inflate) { 1016 iis.addDummy(); 1017 in = new PooledInflaterInputStream(iis, bufSize); 1018 if (check) 1019 in = new CheckedInputStream(in, entry, bufSize); 1020 break; 1021 } else { 1022 in = check 1023 ? new RawCheckedInputStream(iis, entry, bufSize) 1024 : (InputStream) iis; 1025 } 1026 break; 1027 1028 case ZipEntry.STORED: 1029 in = check 1030 ? new CheckedInputStream(iis, entry, bufSize) 1031 : (InputStream) iis; 1032 break; 1033 1034 default: 1035 throw new ZipException(name + ": " + entry.getMethod() 1036 + ": Unsupported compression method!"); 1037 } 1038 1039 return in; 1040 } 1041 1042 private static final int getBufferSize(final ZipEntry entry) { 1043 long size = entry.getSize(); 1044 if (size > FLATER_BUF_LENGTH) 1045 size = FLATER_BUF_LENGTH; 1046 else if (size < FLATER_BUF_LENGTH / 8) 1047 size = FLATER_BUF_LENGTH / 8; 1048 return (int) size; 1049 } 1050 1051 1054 private final void ensureOpen() throws ZipException { 1055 if (archive == null) 1056 throw new ZipException("ZipFile has been closed!"); 1057 } 1058 1059 private static final class PooledInflaterInputStream 1060 extends InflaterInputStream { 1061 private boolean closed; 1062 1063 public PooledInflaterInputStream(InputStream in, int size) { 1064 super(in, allocateInflater(), size); 1065 } 1066 1067 public void close() throws IOException { 1068 if (!closed) { 1069 closed = true; 1070 try { 1071 super.close(); 1072 } finally { 1073 releaseInflater(inf); 1074 } 1075 } 1076 } 1077 } 1079 private static Inflater allocateInflater() { 1080 Inflater inflater = null; 1081 1082 synchronized (releasedInflaters) { 1083 for (Iterator i = releasedInflaters.iterator(); i.hasNext(); ) { 1084 inflater = (Inflater) ((Reference) i.next()).get(); 1085 i.remove(); 1086 if (inflater != null) { 1087 inflater.reset(); 1088 break; 1089 } 1090 } 1091 if (inflater == null) 1092 inflater = new Inflater(true); 1093 1094 allocatedInflaters.add(inflater); 1103 } 1104 1105 return inflater; 1106 } 1107 1108 private static void releaseInflater(Inflater inflater) { 1109 synchronized (releasedInflaters) { 1111 releasedInflaters.add(new SoftReference(inflater)); 1112 allocatedInflaters.remove(inflater); 1113 } 1114 } 1115 1116 private static final class CheckedInputStream 1117 extends java.util.zip.CheckedInputStream { 1118 private final ZipEntry entry; 1119 private final int size; 1120 1121 public CheckedInputStream( 1122 final InputStream in, 1123 final ZipEntry entry, 1124 final int size) { 1125 super(in, new CRC32()); 1126 this.entry = entry; 1127 this.size = size; 1128 } 1129 1130 public long skip(long toSkip) throws IOException { 1131 return skipWithBuffer(this, toSkip, new byte[size]); 1132 } 1133 1134 public void close() throws IOException { 1135 try { 1136 skip(Long.MAX_VALUE); } finally { 1138 super.close(); 1139 } 1140 long expectedCrc = entry.getCrc(); 1141 long actualCrc = getChecksum().getValue(); 1142 if (expectedCrc != actualCrc) 1143 throw new CRC32Exception( 1144 entry.getName(), expectedCrc, actualCrc); 1145 } 1146 } 1148 1152 private static long skipWithBuffer( 1153 final InputStream in, 1154 long toSkip, 1155 final byte[] buf) 1156 throws IOException { 1157 long total = 0; 1158 while (toSkip > 0) { 1159 final int skipped = in.read(buf, 0, 1161 toSkip < buf.length ? (int) toSkip : buf.length); 1162 if (skipped < 0) 1163 return total; 1164 total += skipped; 1165 toSkip -= skipped; 1166 } 1167 return total; 1168 } 1169 1170 1175 private static final class RawCheckedInputStream extends FilterInputStream { 1176 1177 private final Checksum checksum = new CRC32(); 1178 private final byte[] singleByteBuf = new byte[1]; 1179 private final Inflater inf; 1180 private final byte[] infBuf; private final ZipEntry entry; 1182 private boolean closed; 1183 1184 public RawCheckedInputStream( 1185 final InputStream in, 1186 final ZipEntry entry, 1187 final int size) { 1188 super(in); 1189 this.inf = allocateInflater(); 1190 this.infBuf = new byte[size]; 1191 this.entry = entry; 1192 } 1193 1194 private void ensureOpen() throws IOException { 1195 if (closed) 1196 throw new IOException("Input stream has been closed!"); 1197 } 1198 1199 public int read() throws IOException { 1200 int read; 1201 while ((read = read(singleByteBuf, 0, 1)) == 0) ; 1203 return read < 0 ? -1 : singleByteBuf[0] & 0xFF; 1204 } 1205 1206 public int read(byte[] buf, int off, int len) throws IOException { 1207 if (buf == null) 1209 throw new NullPointerException (); 1210 final int offPlusLen = off + len; 1211 if ((off | len | offPlusLen | buf.length - offPlusLen) < 0) 1212 throw new IndexOutOfBoundsException (); 1213 if (len == 0) 1214 return 0; 1215 1216 ensureOpen(); 1218 1219 assert inf.finished() || inf.needsInput(); 1221 assert !inf.needsDictionary(); 1222 1223 final int read = in.read(buf, off, len); 1225 if (read < 0) { 1226 assert inf.finished(); 1228 assert inf.needsInput(); 1229 assert !inf.needsDictionary(); 1230 return read; 1231 } 1232 1233 inf.setInput(buf, off, read); 1235 try { 1236 int inflated; 1237 while ((inflated = inf.inflate(infBuf, 0, infBuf.length)) > 0) 1238 checksum.update(infBuf, 0, inflated); 1239 } catch (DataFormatException failure) { 1240 IOException ioe = new IOException(failure.getLocalizedMessage()); 1241 ioe.initCause(failure); 1242 throw ioe; 1243 } 1244 1245 assert inf.finished() || inf.needsInput(); 1247 assert !inf.needsDictionary(); 1248 1249 return read; 1250 } 1251 1252 public long skip(long toSkip) throws IOException { 1253 return skipWithBuffer(this, toSkip, new byte[infBuf.length]); 1254 } 1255 1256 public void close() throws IOException { 1257 if (closed) 1258 return; 1259 1260 try { 1262 skip(Long.MAX_VALUE); } finally { 1264 closed = true; 1265 releaseInflater(inf); 1266 super.close(); 1267 } 1268 1269 long expectedCrc = entry.getCrc(); 1270 long actualCrc = checksum.getValue(); 1271 if (expectedCrc != actualCrc) 1272 throw new CRC32Exception( 1273 entry.getName(), expectedCrc, actualCrc); 1274 } 1275 1276 public synchronized void mark(int readlimit) { 1277 } 1278 1279 public synchronized void reset() throws IOException { 1280 throw new IOException("mark()/reset() not supported!"); 1281 } 1282 1283 public boolean markSupported() { 1284 return false; 1285 } 1286 } 1288 1294 public void close() throws IOException { 1295 if (archive != null) { 1297 final ReadOnlyFile oldArchive = archive; 1298 archive = null; 1299 oldArchive.close(); 1300 } 1301 } 1302 1303 private static final int readUShort(final byte[] bytes) { 1304 return readUShort(bytes, 0); 1305 } 1306 1307 private static final int readUShort(final byte[] bytes, final int off) { 1308 return ((bytes[off + 1] & 0xFF) << 8) | (bytes[off] & 0xFF); 1309 } 1310 1311 private static final long readUInt(final byte[] bytes) { 1312 return readUInt(bytes, 0); 1313 } 1314 1315 private static final long readUInt(final byte[] bytes, int off) { 1316 off += 3; 1317 long v = bytes[off--] & 0xFFl; 1318 v <<= 8; 1319 v |= bytes[off--] & 0xFFl; 1320 v <<= 8; 1321 v |= bytes[off--] & 0xFFl; 1322 v <<= 8; 1323 v |= bytes[off] & 0xFFl; 1324 return v; 1325 } 1326 1327 1337 private class IntervalInputStream extends AccountedInputStream { 1338 private long remaining; 1339 private long fp; 1340 private boolean addDummyByte; 1341 1342 1347 IntervalInputStream(long start, long remaining) { 1348 assert start >= 0; 1349 assert remaining >= 0; 1350 this.remaining = remaining; 1351 fp = start; 1352 } 1353 1354 public int read() throws IOException { 1355 if (remaining <= 0) { 1356 if (addDummyByte) { 1357 addDummyByte = false; 1358 return 0; 1359 } 1360 1361 return -1; 1362 } 1363 1364 final int ret; 1365 ensureOpen(); 1366 archive.seek(fp); 1367 ret = archive.read(); 1368 if (ret >= 0) { 1369 fp++; 1370 remaining--; 1371 } 1372 1373 return ret; 1374 } 1375 1376 public int read(final byte[] b, final int off, int len) 1377 throws IOException { 1378 if (len <= 0) 1379 return 0; 1380 1381 if (remaining <= 0) { 1382 if (addDummyByte) { 1383 addDummyByte = false; 1384 b[off] = 0; 1385 return 1; 1386 } 1387 1388 return -1; 1389 } 1390 1391 if (len > remaining) 1392 len = (int) remaining; 1393 1394 1395 final int ret; 1396 ensureOpen(); 1397 archive.seek(fp); 1398 ret = archive.read(b, off, len); 1399 if (ret > 0) { 1400 fp += ret; 1401 remaining -= ret; 1402 } 1403 1404 return ret; 1405 } 1406 1407 1411 void addDummy() { 1412 addDummyByte = true; 1413 } 1414 1415 1425 public int available() { 1426 long available = remaining; 1427 if (addDummyByte) 1428 available++; 1429 return available > Integer.MAX_VALUE 1430 ? Integer.MAX_VALUE 1431 : (int) available; 1432 } 1433 } 1435 private abstract class AccountedInputStream extends InputStream { 1436 private boolean closed; 1437 1438 public AccountedInputStream() { 1439 openStreams++; 1440 } 1441 1442 public void close() throws IOException { 1443 if (!closed) { 1445 closed = true; 1446 openStreams--; 1447 super.close(); 1448 } 1449 } 1450 1451 protected void finalize() throws IOException { 1452 close(); 1453 } 1454 }; 1455 1456 private static class OffsetMapper { 1457 public long location(long offset) { 1458 return offset; 1459 } 1460 } 1461 1462 private static class IrregularOffsetMapper extends OffsetMapper { 1463 final long start; 1464 1465 public IrregularOffsetMapper(long start) { 1466 this.start = start; 1467 } 1468 1469 public long location(long offset) { 1470 return start + offset; 1471 } 1472 } 1473} 1474 | Popular Tags |