View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.zip;
20  
21  import java.io.Serializable;
22  import java.nio.file.attribute.FileTime;
23  import java.util.Arrays;
24  import java.util.Date;
25  import java.util.Objects;
26  import java.util.zip.ZipException;
27  
28  import org.apache.commons.compress.utils.TimeUtils;
29  
30  /**
31   * <p>
32   * An extra field that stores additional file and directory timestamp data for ZIP entries. Each ZIP entry can include up to three timestamps (modify, access,
33   * create*). The timestamps are stored as 32 bit signed integers representing seconds since UNIX epoch (Jan 1st, 1970, UTC). This field improves on ZIP's
34   * default timestamp granularity, since it allows one to store additional timestamps, and, in addition, the timestamps are stored using per-second granularity
35   * (zip's default behavior can only store timestamps to the nearest <em>even</em> second).
36   * </p>
37   * <p>
38   * Unfortunately, 32 (signed) bits can only store dates up to the year 2037, and so this extra field will eventually be obsolete. Enjoy it while it lasts!
39   * </p>
40   * <ul>
41   * <li><b>modifyTime:</b> most recent time of file/directory modification (or file/dir creation if the entry has not been modified since it was created).</li>
42   * <li><b>accessTime:</b> most recent time file/directory was opened (e.g., read from disk). Many people disable their operating systems from updating this
43   * value using the NOATIME mount option to optimize disk behavior, and thus it's not always reliable. In those cases it's always equal to modifyTime.</li>
44   * <li><b>*createTime:</b> modern Linux file systems (e.g., ext2 and newer) do not appear to store a value like this, and so it's usually omitted altogether in
45   * the ZIP extra field. Perhaps other Unix systems track this.</li>
46   * </ul>
47   * <p>
48   * We're using the field definition given in Info-Zip's source archive: zip-3.0.tar.gz/proginfo/extrafld.txt
49   * </p>
50   *
51   * <pre>
52   * Value         Size        Description
53   * -----         ----        -----------
54   * 0x5455        Short       tag for this extra block type ("UT")
55   * TSize         Short       total data size for this block
56   * Flags         Byte        info bits
57   * (ModTime)     Long        time of last modification (UTC/GMT)
58   * (AcTime)      Long        time of last access (UTC/GMT)
59   * (CrTime)      Long        time of original creation (UTC/GMT)
60   *
61   * Central-header version:
62   *
63   * Value         Size        Description
64   * -----         ----        -----------
65   * 0x5455        Short       tag for this extra block type ("UT")
66   * TSize         Short       total data size for this block
67   * Flags         Byte        info bits (refers to local header!)
68   * (ModTime)     Long        time of last modification (UTC/GMT)
69   * </pre>
70   *
71   * @since 1.5
72   */
73  public class X5455_ExtendedTimestamp implements ZipExtraField, Cloneable, Serializable {
74      private static final long serialVersionUID = 1L;
75  
76      /**
77       * The header ID for this extra field.
78       *
79       * @since 1.23
80       */
81      public static final ZipShort HEADER_ID = new ZipShort(0x5455);
82  
83      /**
84       * The bit set inside the flags by when the last modification time is present in this extra field.
85       */
86      public static final byte MODIFY_TIME_BIT = 1;
87      /**
88       * The bit set inside the flags by when the lasr access time is present in this extra field.
89       */
90      public static final byte ACCESS_TIME_BIT = 2;
91      /**
92       * The bit set inside the flags by when the original creation time is present in this extra field.
93       */
94      public static final byte CREATE_TIME_BIT = 4;
95  
96      /**
97       * Utility method converts java.util.Date (milliseconds since epoch) into a ZipLong (seconds since epoch).
98       * <p/>
99       * Also makes sure the converted ZipLong is not too big to fit in 32 unsigned bits.
100      *
101      * @param d java.util.Date to convert to ZipLong
102      * @return ZipLong
103      */
104     private static ZipLong dateToZipLong(final Date d) {
105         if (d == null) {
106             return null;
107         }
108         return unixTimeToZipLong(d.getTime() / 1000);
109     }
110 
111     /**
112      * Utility method converts {@link FileTime} into a ZipLong (seconds since epoch).
113      * <p/>
114      * Also makes sure the converted ZipLong is not too big to fit in 32 unsigned bits.
115      *
116      * @param time {@link FileTime} to convert to ZipLong
117      * @return ZipLong
118      */
119     private static ZipLong fileTimeToZipLong(final FileTime time) {
120         return time == null ? null : unixTimeToZipLong(TimeUtils.toUnixTime(time));
121     }
122 
123     private static FileTime unixTimeToFileTime(final ZipLong unixTime) {
124         return unixTime != null ? TimeUtils.unixTimeToFileTime(unixTime.getIntValue()) : null;
125     }
126     // The 3 boolean fields (below) come from this flag's byte. The remaining 5 bits
127     // are ignored according to the current version of the spec (December 2012).
128 
129     private static ZipLong unixTimeToZipLong(final long unixTime) {
130         if (!TimeUtils.isUnixTime(unixTime)) {
131             throw new IllegalArgumentException("X5455 timestamps must fit in a signed 32 bit integer: " + unixTime);
132         }
133         return new ZipLong(unixTime);
134     }
135 
136     private static Date zipLongToDate(final ZipLong unixTime) {
137         return unixTime != null ? new Date(unixTime.getIntValue() * 1000L) : null;
138     }
139 
140     private byte flags;
141     // Note: even if bit1 and bit2 are set, the Central data will still not contain
142     // access/create fields: only local data ever holds those! This causes
143     // some of our implementation to look a little odd, with seemingly spurious
144     // != null and length checks.
145     private boolean bit0_modifyTimePresent;
146     private boolean bit1_accessTimePresent;
147 
148     private boolean bit2_createTimePresent;
149 
150     private ZipLong modifyTime;
151 
152     private ZipLong accessTime;
153 
154     private ZipLong createTime;
155 
156     /**
157      * Constructor for X5455_ExtendedTimestamp.
158      */
159     public X5455_ExtendedTimestamp() {
160     }
161 
162     @Override
163     public Object clone() throws CloneNotSupportedException {
164         return super.clone();
165     }
166 
167     @Override
168     public boolean equals(final Object o) {
169         if (o instanceof X5455_ExtendedTimestamp) {
170             final X5455_ExtendedTimestamp xf = (X5455_ExtendedTimestamp) o;
171 
172             // The ZipLong==ZipLong clauses handle the cases where both are null.
173             // and only last 3 bits of flags matter.
174             return (flags & 0x07) == (xf.flags & 0x07) && Objects.equals(modifyTime, xf.modifyTime) && Objects.equals(accessTime, xf.accessTime)
175                     && Objects.equals(createTime, xf.createTime);
176         }
177         return false;
178     }
179 
180     /**
181      * Gets the access time as a {@link FileTime} of this ZIP entry, or null if no such timestamp exists in the ZIP entry. The milliseconds are always zeroed
182      * out, since the underlying data offers only per-second precision.
183      *
184      * @return modify time as {@link FileTime} or null.
185      * @since 1.23
186      */
187     public FileTime getAccessFileTime() {
188         return unixTimeToFileTime(accessTime);
189     }
190 
191     /**
192      * Gets the access time as a java.util.Date of this ZIP entry, or null if no such timestamp exists in the ZIP entry. The milliseconds are always zeroed out,
193      * since the underlying data offers only per-second precision.
194      *
195      * @return access time as java.util.Date or null.
196      */
197     public Date getAccessJavaTime() {
198         return zipLongToDate(accessTime);
199     }
200 
201     /**
202      * Gets the access time (seconds since epoch) of this ZIP entry as a ZipLong object, or null if no such timestamp exists in the ZIP entry.
203      *
204      * @return access time (seconds since epoch) or null.
205      */
206     public ZipLong getAccessTime() {
207         return accessTime;
208     }
209 
210     /**
211      * Gets the actual data to put into central directory data - without Header-ID or length specifier.
212      *
213      * @return the central directory data
214      */
215     @Override
216     public byte[] getCentralDirectoryData() {
217         // Truncate out create & access time (last 8 bytes) from
218         // the copy of the local data we obtained:
219         return Arrays.copyOf(getLocalFileDataData(), getCentralDirectoryLength().getValue());
220     }
221 
222     /**
223      * Gets the length of the extra field in the local file data - without Header-ID or length specifier.
224      *
225      * <p>
226      * For X5455 the central length is often smaller than the local length, because central cannot contain access or create timestamps.
227      * </p>
228      *
229      * @return a {@code ZipShort} for the length of the data of this extra field
230      */
231     @Override
232     public ZipShort getCentralDirectoryLength() {
233         return new ZipShort(1 + (bit0_modifyTimePresent ? 4 : 0));
234     }
235 
236     /**
237      * Gets the create time as a {@link FileTime} of this ZIP entry, or null if no such timestamp exists in the ZIP entry. The milliseconds are always zeroed
238      * out, since the underlying data offers only per-second precision.
239      *
240      * @return modify time as {@link FileTime} or null.
241      * @since 1.23
242      */
243     public FileTime getCreateFileTime() {
244         return unixTimeToFileTime(createTime);
245     }
246 
247     /**
248      * <p>
249      * Gets the create time as a java.util.Date of this ZIP entry, or null if no such timestamp exists in the ZIP entry. The milliseconds are always zeroed out,
250      * since the underlying data offers only per-second precision.
251      * </p>
252      * <p>
253      * Note: modern Linux file systems (e.g., ext2) do not appear to store a "create time" value, and so it's usually omitted altogether in the ZIP extra field.
254      * Perhaps other Unix systems track this.
255      * </p>
256      *
257      * @return create time as java.util.Date or null.
258      */
259     public Date getCreateJavaTime() {
260         return zipLongToDate(createTime);
261     }
262 
263     /**
264      * <p>
265      * Gets the create time (seconds since epoch) of this ZIP entry as a ZipLong object, or null if no such timestamp exists in the ZIP entry.
266      * </p>
267      * <p>
268      * Note: modern Linux file systems (e.g., ext2) do not appear to store a "create time" value, and so it's usually omitted altogether in the ZIP extra field.
269      * Perhaps other Unix systems track this.
270      * </p>
271      *
272      * @return create time (seconds since epoch) or null.
273      */
274     public ZipLong getCreateTime() {
275         return createTime;
276     }
277 
278     /**
279      * Gets flags byte. The flags byte tells us which of the three datestamp fields are present in the data:
280      *
281      * <pre>
282      * bit0 - modify time
283      * bit1 - access time
284      * bit2 - create time
285      * </pre>
286      *
287      * Only first 3 bits of flags are used according to the latest version of the spec (December 2012).
288      *
289      * @return flags byte indicating which of the three datestamp fields are present.
290      */
291     public byte getFlags() {
292         return flags;
293     }
294 
295     /**
296      * Gets the Header-ID.
297      *
298      * @return the value for the header id for this extrafield
299      */
300     @Override
301     public ZipShort getHeaderId() {
302         return HEADER_ID;
303     }
304 
305     /**
306      * Gets the actual data to put into local file data - without Header-ID or length specifier.
307      *
308      * @return get the data
309      */
310     @Override
311     public byte[] getLocalFileDataData() {
312         final byte[] data = new byte[getLocalFileDataLength().getValue()];
313         int pos = 0;
314         data[pos++] = 0;
315         if (bit0_modifyTimePresent) {
316             data[0] |= MODIFY_TIME_BIT;
317             System.arraycopy(modifyTime.getBytes(), 0, data, pos, 4);
318             pos += 4;
319         }
320         if (bit1_accessTimePresent && accessTime != null) {
321             data[0] |= ACCESS_TIME_BIT;
322             System.arraycopy(accessTime.getBytes(), 0, data, pos, 4);
323             pos += 4;
324         }
325         if (bit2_createTimePresent && createTime != null) {
326             data[0] |= CREATE_TIME_BIT;
327             System.arraycopy(createTime.getBytes(), 0, data, pos, 4);
328             pos += 4; // NOSONAR - assignment as documentation
329         }
330         return data;
331     }
332 
333     /**
334      * Gets the length of the extra field in the local file data - without Header-ID or length specifier.
335      *
336      * @return a {@code ZipShort} for the length of the data of this extra field
337      */
338     @Override
339     public ZipShort getLocalFileDataLength() {
340         return new ZipShort(1 + (bit0_modifyTimePresent ? 4 : 0) + (bit1_accessTimePresent && accessTime != null ? 4 : 0)
341                 + (bit2_createTimePresent && createTime != null ? 4 : 0));
342     }
343 
344     /**
345      * Gets the modify time as a {@link FileTime} of this ZIP entry, or null if no such timestamp exists in the ZIP entry. The milliseconds are always zeroed
346      * out, since the underlying data offers only per-second precision.
347      *
348      * @return modify time as {@link FileTime} or null.
349      * @since 1.23
350      */
351     public FileTime getModifyFileTime() {
352         return unixTimeToFileTime(modifyTime);
353     }
354 
355     /**
356      * Gets the modify time as a java.util.Date of this ZIP entry, or null if no such timestamp exists in the ZIP entry. The milliseconds are always zeroed out,
357      * since the underlying data offers only per-second precision.
358      *
359      * @return modify time as java.util.Date or null.
360      */
361     public Date getModifyJavaTime() {
362         return zipLongToDate(modifyTime);
363     }
364 
365     /**
366      * Gets the modify time (seconds since epoch) of this ZIP entry as a ZipLong object, or null if no such timestamp exists in the ZIP entry.
367      *
368      * @return modify time (seconds since epoch) or null.
369      */
370     public ZipLong getModifyTime() {
371         return modifyTime;
372     }
373 
374     @Override
375     public int hashCode() {
376         int hc = -123 * (flags & 0x07); // only last 3 bits of flags matter
377         if (modifyTime != null) {
378             hc ^= modifyTime.hashCode();
379         }
380         if (accessTime != null) {
381             // Since accessTime is often same as modifyTime,
382             // this prevents them from XOR negating each other.
383             hc ^= Integer.rotateLeft(accessTime.hashCode(), 11);
384         }
385         if (createTime != null) {
386             hc ^= Integer.rotateLeft(createTime.hashCode(), 22);
387         }
388         return hc;
389     }
390 
391     /**
392      * Tests whether bit0 of the flags byte is set or not, which should correspond to the presence or absence of a modify timestamp in this particular ZIP
393      * entry.
394      *
395      * @return true if bit0 of the flags byte is set.
396      */
397     public boolean isBit0_modifyTimePresent() {
398         return bit0_modifyTimePresent;
399     }
400 
401     /**
402      * Tests whether bit1 of the flags byte is set or not, which should correspond to the presence or absence of a "last access" timestamp in this particular
403      * ZIP entry.
404      *
405      * @return true if bit1 of the flags byte is set.
406      */
407     public boolean isBit1_accessTimePresent() {
408         return bit1_accessTimePresent;
409     }
410 
411     /**
412      * Tests whether bit2 of the flags byte is set or not, which should correspond to the presence or absence of a create timestamp in this particular ZIP
413      * entry.
414      *
415      * @return true if bit2 of the flags byte is set.
416      */
417     public boolean isBit2_createTimePresent() {
418         return bit2_createTimePresent;
419     }
420 
421     /**
422      * Doesn't do anything special since this class always uses the same parsing logic for both central directory and local file data.
423      */
424     @Override
425     public void parseFromCentralDirectoryData(final byte[] buffer, final int offset, final int length) throws ZipException {
426         reset();
427         parseFromLocalFileData(buffer, offset, length);
428     }
429 
430     /**
431      * Populate data from this array as if it was in local file data.
432      *
433      * @param data   an array of bytes
434      * @param offset the start offset
435      * @param length the number of bytes in the array from offset
436      * @throws java.util.zip.ZipException on error
437      */
438     @Override
439     public void parseFromLocalFileData(final byte[] data, int offset, final int length) throws ZipException {
440         reset();
441         if (length < 1) {
442             throw new ZipException("X5455_ExtendedTimestamp too short, only " + length + " bytes");
443         }
444         final int len = offset + length;
445         setFlags(data[offset++]);
446         if (bit0_modifyTimePresent && offset + 4 <= len) {
447             modifyTime = new ZipLong(data, offset);
448             offset += 4;
449         } else {
450             bit0_modifyTimePresent = false;
451         }
452         if (bit1_accessTimePresent && offset + 4 <= len) {
453             accessTime = new ZipLong(data, offset);
454             offset += 4;
455         } else {
456             bit1_accessTimePresent = false;
457         }
458         if (bit2_createTimePresent && offset + 4 <= len) {
459             createTime = new ZipLong(data, offset);
460             offset += 4; // NOSONAR - assignment as documentation
461         } else {
462             bit2_createTimePresent = false;
463         }
464     }
465 
466     /**
467      * Reset state back to newly constructed state. Helps us make sure parse() calls always generate clean results.
468      */
469     private void reset() {
470         setFlags((byte) 0);
471         this.modifyTime = null;
472         this.accessTime = null;
473         this.createTime = null;
474     }
475 
476     /**
477      * <p>
478      * Sets the acccess time as a {@link FileTime} of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
479      * </p>
480      * <p>
481      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
482      * flags is also set.
483      * </p>
484      *
485      * @param time access time as {@link FileTime}
486      * @since 1.23
487      */
488     public void setAccessFileTime(final FileTime time) {
489         setAccessTime(fileTimeToZipLong(time));
490     }
491 
492     /**
493      * <p>
494      * Sets the access time as a java.util.Date of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
495      * </p>
496      * <p>
497      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
498      * flags is also set.
499      * </p>
500      *
501      * @param d access time as java.util.Date
502      */
503     public void setAccessJavaTime(final Date d) {
504         setAccessTime(dateToZipLong(d));
505     }
506 
507     /**
508      * <p>
509      * Sets the access time (seconds since epoch) of this ZIP entry using a ZipLong object
510      * </p>
511      * <p>
512      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
513      * flags is also set.
514      * </p>
515      *
516      * @param l ZipLong of the access time (seconds per epoch)
517      */
518     public void setAccessTime(final ZipLong l) {
519         bit1_accessTimePresent = l != null;
520         flags = (byte) (l != null ? flags | ACCESS_TIME_BIT : flags & ~ACCESS_TIME_BIT);
521         this.accessTime = l;
522     }
523 
524     /**
525      * <p>
526      * Sets the create time as a {@link FileTime} of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
527      * </p>
528      * <p>
529      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
530      * flags is also set.
531      * </p>
532      *
533      * @param time create time as {@link FileTime}
534      * @since 1.23
535      */
536     public void setCreateFileTime(final FileTime time) {
537         setCreateTime(fileTimeToZipLong(time));
538     }
539 
540     /**
541      * <p>
542      * Sets the create time as a java.util.Date of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
543      * </p>
544      * <p>
545      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
546      * flags is also set.
547      * </p>
548      *
549      * @param d create time as java.util.Date
550      */
551     public void setCreateJavaTime(final Date d) {
552         setCreateTime(dateToZipLong(d));
553     }
554 
555     /**
556      * <p>
557      * Sets the create time (seconds since epoch) of this ZIP entry using a ZipLong object
558      * </p>
559      * <p>
560      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
561      * flags is also set.
562      * </p>
563      *
564      * @param l ZipLong of the create time (seconds per epoch)
565      */
566     public void setCreateTime(final ZipLong l) {
567         bit2_createTimePresent = l != null;
568         flags = (byte) (l != null ? flags | CREATE_TIME_BIT : flags & ~CREATE_TIME_BIT);
569         this.createTime = l;
570     }
571 
572     /**
573      * Sets flags byte. The flags byte tells us which of the three datestamp fields are present in the data:
574      *
575      * <pre>
576      * bit0 - modify time
577      * bit1 - access time
578      * bit2 - create time
579      * </pre>
580      *
581      * Only first 3 bits of flags are used according to the latest version of the spec (December 2012).
582      *
583      * @param flags flags byte indicating which of the three datestamp fields are present.
584      */
585     public void setFlags(final byte flags) {
586         this.flags = flags;
587         this.bit0_modifyTimePresent = (flags & MODIFY_TIME_BIT) == MODIFY_TIME_BIT;
588         this.bit1_accessTimePresent = (flags & ACCESS_TIME_BIT) == ACCESS_TIME_BIT;
589         this.bit2_createTimePresent = (flags & CREATE_TIME_BIT) == CREATE_TIME_BIT;
590     }
591 
592     /**
593      * <p>
594      * Sets the modify time as a {@link FileTime} of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
595      * </p>
596      * <p>
597      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
598      * flags is also set.
599      * </p>
600      *
601      * @param time modify time as {@link FileTime}
602      * @since 1.23
603      */
604     public void setModifyFileTime(final FileTime time) {
605         setModifyTime(fileTimeToZipLong(time));
606     }
607 
608     /**
609      * <p>
610      * Sets the modify time as a java.util.Date of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
611      * </p>
612      * <p>
613      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
614      * flags is also set.
615      * </p>
616      *
617      * @param d modify time as java.util.Date
618      */
619     public void setModifyJavaTime(final Date d) {
620         setModifyTime(dateToZipLong(d));
621     }
622 
623     /**
624      * <p>
625      * Sets the modify time (seconds since epoch) of this ZIP entry using a ZipLong object.
626      * </p>
627      * <p>
628      * Note: the setters for flags and timestamps are decoupled. Even if the timestamp is not-null, it will only be written out if the corresponding bit in the
629      * flags is also set.
630      * </p>
631      *
632      * @param l ZipLong of the modify time (seconds per epoch)
633      */
634     public void setModifyTime(final ZipLong l) {
635         bit0_modifyTimePresent = l != null;
636         flags = (byte) (l != null ? flags | MODIFY_TIME_BIT : flags & ~MODIFY_TIME_BIT);
637         this.modifyTime = l;
638     }
639 
640     /**
641      * Returns a String representation of this class useful for debugging purposes.
642      *
643      * @return A String representation of this class useful for debugging purposes.
644      */
645     @Override
646     public String toString() {
647         final StringBuilder buf = new StringBuilder();
648         buf.append("0x5455 Zip Extra Field: Flags=");
649         buf.append(Integer.toBinaryString(ZipUtil.unsignedIntToSignedByte(flags))).append(" ");
650         if (bit0_modifyTimePresent && modifyTime != null) {
651             final Date m = getModifyJavaTime();
652             buf.append(" Modify:[").append(m).append("] ");
653         }
654         if (bit1_accessTimePresent && accessTime != null) {
655             final Date a = getAccessJavaTime();
656             buf.append(" Access:[").append(a).append("] ");
657         }
658         if (bit2_createTimePresent && createTime != null) {
659             final Date c = getCreateJavaTime();
660             buf.append(" Create:[").append(c).append("] ");
661         }
662         return buf.toString();
663     }
664 
665 }