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   *   https://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.io.file.attribute.FileTimes;
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><strong>modifyTime:</strong> most recent time of file/directory modification (or file/dir creation if the entry has not been modified since it was
42   * created).</li>
43   * <li><strong>accessTime:</strong> most recent time file/directory was opened (for example, read from disk). Many people disable their operating systems from
44   * updating this 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
45   * modifyTime.</li>
46   * <li><strong>*createTime:</strong> modern Linux file systems (for example, ext2 and newer) do not appear to store a value like this, and so it's usually
47   * omitted altogether in the ZIP extra field. Perhaps other Unix systems track this.</li>
48   * </ul>
49   * <p>
50   * We're using the field definition given in Info-Zip's source archive: zip-3.0.tar.gz/proginfo/extrafld.txt
51   * </p>
52   *
53   * <pre>
54   * Value         Size        Description
55   * -----         ----        -----------
56   * 0x5455        Short       tag for this extra block type ("UT")
57   * TSize         Short       total data size for this block
58   * Flags         Byte        info bits
59   * (ModTime)     Long        time of last modification (UTC/GMT)
60   * (AcTime)      Long        time of last access (UTC/GMT)
61   * (CrTime)      Long        time of original creation (UTC/GMT)
62   *
63   * Central-header version:
64   *
65   * Value         Size        Description
66   * -----         ----        -----------
67   * 0x5455        Short       tag for this extra block type ("UT")
68   * TSize         Short       total data size for this block
69   * Flags         Byte        info bits (refers to local header!)
70   * (ModTime)     Long        time of last modification (UTC/GMT)
71   * </pre>
72   *
73   * @since 1.5
74   */
75  public class X5455_ExtendedTimestamp implements ZipExtraField, Cloneable, Serializable {
76      private static final long serialVersionUID = 1L;
77      /**
78       * The header ID for this extra field.
79       *
80       * @since 1.23
81       */
82      public static final ZipShort HEADER_ID = new ZipShort(0x5455);
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(FileTimes.toUnixTime(time));
121     }
122 
123     private static FileTime unixTimeToFileTime(final ZipLong unixTime) {
124         return unixTime != null ? FileTimes.fromUnixTime(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 (!FileTimes.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     private boolean bit2_createTimePresent;
148     private ZipLong modifyTime;
149     private ZipLong accessTime;
150     private ZipLong createTime;
151 
152     /**
153      * Constructor for X5455_ExtendedTimestamp.
154      */
155     public X5455_ExtendedTimestamp() {
156     }
157 
158     @Override
159     public Object clone() throws CloneNotSupportedException {
160         return super.clone();
161     }
162 
163     @Override
164     public boolean equals(final Object o) {
165         if (o instanceof X5455_ExtendedTimestamp) {
166             final X5455_ExtendedTimestamp xf = (X5455_ExtendedTimestamp) o;
167             // The ZipLong==ZipLong clauses handle the cases where both are null.
168             // and only last 3 bits of flags matter.
169             return (flags & 0x07) == (xf.flags & 0x07) && Objects.equals(modifyTime, xf.modifyTime) && Objects.equals(accessTime, xf.accessTime)
170                     && Objects.equals(createTime, xf.createTime);
171         }
172         return false;
173     }
174 
175     /**
176      * 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
177      * out, since the underlying data offers only per-second precision.
178      *
179      * @return modify time as {@link FileTime} or null.
180      * @since 1.23
181      */
182     public FileTime getAccessFileTime() {
183         return unixTimeToFileTime(accessTime);
184     }
185 
186     /**
187      * 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,
188      * since the underlying data offers only per-second precision.
189      *
190      * @return access time as java.util.Date or null.
191      */
192     public Date getAccessJavaTime() {
193         return zipLongToDate(accessTime);
194     }
195 
196     /**
197      * 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.
198      *
199      * @return access time (seconds since epoch) or null.
200      */
201     public ZipLong getAccessTime() {
202         return accessTime;
203     }
204 
205     /**
206      * Gets the actual data to put into central directory data - without Header-ID or length specifier.
207      *
208      * @return the central directory data
209      */
210     @Override
211     public byte[] getCentralDirectoryData() {
212         // Truncate out create & access time (last 8 bytes) from
213         // the copy of the local data we obtained:
214         return Arrays.copyOf(getLocalFileDataData(), getCentralDirectoryLength().getValue());
215     }
216 
217     /**
218      * Gets the length of the extra field in the local file data - without Header-ID or length specifier.
219      *
220      * <p>
221      * For X5455 the central length is often smaller than the local length, because central cannot contain access or create timestamps.
222      * </p>
223      *
224      * @return a {@code ZipShort} for the length of the data of this extra field
225      */
226     @Override
227     public ZipShort getCentralDirectoryLength() {
228         return new ZipShort(1 + (bit0_modifyTimePresent ? 4 : 0));
229     }
230 
231     /**
232      * 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
233      * out, since the underlying data offers only per-second precision.
234      *
235      * @return modify time as {@link FileTime} or null.
236      * @since 1.23
237      */
238     public FileTime getCreateFileTime() {
239         return unixTimeToFileTime(createTime);
240     }
241 
242     /**
243      * <p>
244      * 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,
245      * since the underlying data offers only per-second precision.
246      * </p>
247      * <p>
248      * Note: modern Linux file systems (for example, ext2) do not appear to store a "create time" value, and so it's usually omitted altogether in the ZIP extra
249      * field. Perhaps other Unix systems track this.
250      * </p>
251      *
252      * @return create time as java.util.Date or null.
253      */
254     public Date getCreateJavaTime() {
255         return zipLongToDate(createTime);
256     }
257 
258     /**
259      * <p>
260      * 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.
261      * </p>
262      * <p>
263      * Note: modern Linux file systems (for example, ext2) do not appear to store a "create time" value, and so it's usually omitted altogether in the ZIP extra
264      * field. Perhaps other Unix systems track this.
265      * </p>
266      *
267      * @return create time (seconds since epoch) or null.
268      */
269     public ZipLong getCreateTime() {
270         return createTime;
271     }
272 
273     /**
274      * Gets flags byte. The flags byte tells us which of the three datestamp fields are present in the data:
275      *
276      * <pre>
277      * bit0 - modify time
278      * bit1 - access time
279      * bit2 - create time
280      * </pre>
281      *
282      * Only first 3 bits of flags are used according to the latest version of the spec (December 2012).
283      *
284      * @return flags byte indicating which of the three datestamp fields are present.
285      */
286     public byte getFlags() {
287         return flags;
288     }
289 
290     /**
291      * Gets the Header-ID.
292      *
293      * @return the value for the header id for this extrafield
294      */
295     @Override
296     public ZipShort getHeaderId() {
297         return HEADER_ID;
298     }
299 
300     /**
301      * Gets the actual data to put into local file data - without Header-ID or length specifier.
302      *
303      * @return get the data
304      */
305     @Override
306     public byte[] getLocalFileDataData() {
307         final byte[] data = new byte[getLocalFileDataLength().getValue()];
308         int pos = 0;
309         data[pos++] = 0;
310         if (bit0_modifyTimePresent) {
311             data[0] |= MODIFY_TIME_BIT;
312             System.arraycopy(modifyTime.getBytes(), 0, data, pos, 4);
313             pos += 4;
314         }
315         if (bit1_accessTimePresent && accessTime != null) {
316             data[0] |= ACCESS_TIME_BIT;
317             System.arraycopy(accessTime.getBytes(), 0, data, pos, 4);
318             pos += 4;
319         }
320         if (bit2_createTimePresent && createTime != null) {
321             data[0] |= CREATE_TIME_BIT;
322             System.arraycopy(createTime.getBytes(), 0, data, pos, 4);
323             pos += 4; // NOSONAR - assignment as documentation
324         }
325         return data;
326     }
327 
328     /**
329      * Gets the length of the extra field in the local file data - without Header-ID or length specifier.
330      *
331      * @return a {@code ZipShort} for the length of the data of this extra field
332      */
333     @Override
334     public ZipShort getLocalFileDataLength() {
335         return new ZipShort(1 + (bit0_modifyTimePresent ? 4 : 0) + (bit1_accessTimePresent && accessTime != null ? 4 : 0)
336                 + (bit2_createTimePresent && createTime != null ? 4 : 0));
337     }
338 
339     /**
340      * 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
341      * out, since the underlying data offers only per-second precision.
342      *
343      * @return modify time as {@link FileTime} or null.
344      * @since 1.23
345      */
346     public FileTime getModifyFileTime() {
347         return unixTimeToFileTime(modifyTime);
348     }
349 
350     /**
351      * 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,
352      * since the underlying data offers only per-second precision.
353      *
354      * @return modify time as java.util.Date or null.
355      */
356     public Date getModifyJavaTime() {
357         return zipLongToDate(modifyTime);
358     }
359 
360     /**
361      * 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.
362      *
363      * @return modify time (seconds since epoch) or null.
364      */
365     public ZipLong getModifyTime() {
366         return modifyTime;
367     }
368 
369     @Override
370     public int hashCode() {
371         int hc = -123 * (flags & 0x07); // only last 3 bits of flags matter
372         if (modifyTime != null) {
373             hc ^= modifyTime.hashCode();
374         }
375         if (accessTime != null) {
376             // Since accessTime is often same as modifyTime,
377             // this prevents them from XOR negating each other.
378             hc ^= Integer.rotateLeft(accessTime.hashCode(), 11);
379         }
380         if (createTime != null) {
381             hc ^= Integer.rotateLeft(createTime.hashCode(), 22);
382         }
383         return hc;
384     }
385 
386     /**
387      * 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
388      * entry.
389      *
390      * @return true if bit0 of the flags byte is set.
391      */
392     public boolean isBit0_modifyTimePresent() {
393         return bit0_modifyTimePresent;
394     }
395 
396     /**
397      * 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
398      * ZIP entry.
399      *
400      * @return true if bit1 of the flags byte is set.
401      */
402     public boolean isBit1_accessTimePresent() {
403         return bit1_accessTimePresent;
404     }
405 
406     /**
407      * 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
408      * entry.
409      *
410      * @return true if bit2 of the flags byte is set.
411      */
412     public boolean isBit2_createTimePresent() {
413         return bit2_createTimePresent;
414     }
415 
416     /**
417      * Doesn't do anything special since this class always uses the same parsing logic for both central directory and local file data.
418      */
419     @Override
420     public void parseFromCentralDirectoryData(final byte[] buffer, final int offset, final int length) throws ZipException {
421         reset();
422         parseFromLocalFileData(buffer, offset, length);
423     }
424 
425     /**
426      * Populate data from this array as if it was in local file data.
427      *
428      * @param data   an array of bytes
429      * @param offset the start offset
430      * @param length the number of bytes in the array from offset
431      * @throws java.util.zip.ZipException on error
432      */
433     @Override
434     public void parseFromLocalFileData(final byte[] data, int offset, final int length) throws ZipException {
435         reset();
436         if (length < 1) {
437             throw new ZipException("X5455_ExtendedTimestamp too short, only " + length + " bytes");
438         }
439         final int len = offset + length;
440         setFlags(data[offset++]);
441         if (bit0_modifyTimePresent && offset + 4 <= len) {
442             modifyTime = new ZipLong(data, offset);
443             offset += 4;
444         } else {
445             bit0_modifyTimePresent = false;
446         }
447         if (bit1_accessTimePresent && offset + 4 <= len) {
448             accessTime = new ZipLong(data, offset);
449             offset += 4;
450         } else {
451             bit1_accessTimePresent = false;
452         }
453         if (bit2_createTimePresent && offset + 4 <= len) {
454             createTime = new ZipLong(data, offset);
455             offset += 4; // NOSONAR - assignment as documentation
456         } else {
457             bit2_createTimePresent = false;
458         }
459     }
460 
461     /**
462      * Reset state back to newly constructed state. Helps us make sure parse() calls always generate clean results.
463      */
464     private void reset() {
465         setFlags((byte) 0);
466         this.modifyTime = null;
467         this.accessTime = null;
468         this.createTime = null;
469     }
470 
471     /**
472      * <p>
473      * Sets the acccess time as a {@link FileTime} of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
474      * </p>
475      * <p>
476      * 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
477      * flags is also set.
478      * </p>
479      *
480      * @param time access time as {@link FileTime}
481      * @since 1.23
482      */
483     public void setAccessFileTime(final FileTime time) {
484         setAccessTime(fileTimeToZipLong(time));
485     }
486 
487     /**
488      * <p>
489      * Sets the access time as a java.util.Date of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
490      * </p>
491      * <p>
492      * 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
493      * flags is also set.
494      * </p>
495      *
496      * @param d access time as java.util.Date
497      */
498     public void setAccessJavaTime(final Date d) {
499         setAccessTime(dateToZipLong(d));
500     }
501 
502     /**
503      * <p>
504      * Sets the access time (seconds since epoch) of this ZIP entry using a ZipLong object
505      * </p>
506      * <p>
507      * 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
508      * flags is also set.
509      * </p>
510      *
511      * @param l ZipLong of the access time (seconds per epoch)
512      */
513     public void setAccessTime(final ZipLong l) {
514         bit1_accessTimePresent = l != null;
515         flags = (byte) (l != null ? flags | ACCESS_TIME_BIT : flags & ~ACCESS_TIME_BIT);
516         this.accessTime = l;
517     }
518 
519     /**
520      * <p>
521      * Sets the create time as a {@link FileTime} of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
522      * </p>
523      * <p>
524      * 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
525      * flags is also set.
526      * </p>
527      *
528      * @param time create time as {@link FileTime}
529      * @since 1.23
530      */
531     public void setCreateFileTime(final FileTime time) {
532         setCreateTime(fileTimeToZipLong(time));
533     }
534 
535     /**
536      * <p>
537      * Sets the create time as a java.util.Date of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
538      * </p>
539      * <p>
540      * 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
541      * flags is also set.
542      * </p>
543      *
544      * @param d create time as java.util.Date
545      */
546     public void setCreateJavaTime(final Date d) {
547         setCreateTime(dateToZipLong(d));
548     }
549 
550     /**
551      * <p>
552      * Sets the create time (seconds since epoch) of this ZIP entry using a ZipLong object
553      * </p>
554      * <p>
555      * 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
556      * flags is also set.
557      * </p>
558      *
559      * @param l ZipLong of the create time (seconds per epoch)
560      */
561     public void setCreateTime(final ZipLong l) {
562         bit2_createTimePresent = l != null;
563         flags = (byte) (l != null ? flags | CREATE_TIME_BIT : flags & ~CREATE_TIME_BIT);
564         this.createTime = l;
565     }
566 
567     /**
568      * Sets flags byte. The flags byte tells us which of the three datestamp fields are present in the data:
569      *
570      * <pre>
571      * bit0 - modify time
572      * bit1 - access time
573      * bit2 - create time
574      * </pre>
575      *
576      * Only first 3 bits of flags are used according to the latest version of the spec (December 2012).
577      *
578      * @param flags flags byte indicating which of the three datestamp fields are present.
579      */
580     public void setFlags(final byte flags) {
581         this.flags = flags;
582         this.bit0_modifyTimePresent = (flags & MODIFY_TIME_BIT) == MODIFY_TIME_BIT;
583         this.bit1_accessTimePresent = (flags & ACCESS_TIME_BIT) == ACCESS_TIME_BIT;
584         this.bit2_createTimePresent = (flags & CREATE_TIME_BIT) == CREATE_TIME_BIT;
585     }
586 
587     /**
588      * <p>
589      * Sets the modify time as a {@link FileTime} of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
590      * </p>
591      * <p>
592      * 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
593      * flags is also set.
594      * </p>
595      *
596      * @param time modify time as {@link FileTime}
597      * @since 1.23
598      */
599     public void setModifyFileTime(final FileTime time) {
600         setModifyTime(fileTimeToZipLong(time));
601     }
602 
603     /**
604      * <p>
605      * Sets the modify time as a java.util.Date of this ZIP entry. Supplied value is truncated to per-second precision (milliseconds zeroed-out).
606      * </p>
607      * <p>
608      * 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
609      * flags is also set.
610      * </p>
611      *
612      * @param d modify time as java.util.Date
613      */
614     public void setModifyJavaTime(final Date d) {
615         setModifyTime(dateToZipLong(d));
616     }
617 
618     /**
619      * <p>
620      * Sets the modify time (seconds since epoch) of this ZIP entry using a ZipLong object.
621      * </p>
622      * <p>
623      * 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
624      * flags is also set.
625      * </p>
626      *
627      * @param l ZipLong of the modify time (seconds per epoch)
628      */
629     public void setModifyTime(final ZipLong l) {
630         bit0_modifyTimePresent = l != null;
631         flags = (byte) (l != null ? flags | MODIFY_TIME_BIT : flags & ~MODIFY_TIME_BIT);
632         this.modifyTime = l;
633     }
634 
635     /**
636      * Returns a String representation of this class useful for debugging purposes.
637      *
638      * @return A String representation of this class useful for debugging purposes.
639      */
640     @Override
641     public String toString() {
642         final StringBuilder buf = new StringBuilder();
643         buf.append("0x5455 Zip Extra Field: Flags=");
644         buf.append(Integer.toBinaryString(ZipUtil.unsignedIntToSignedByte(flags))).append(" ");
645         if (bit0_modifyTimePresent && modifyTime != null) {
646             final Date m = getModifyJavaTime();
647             buf.append(" Modify:[").append(m).append("] ");
648         }
649         if (bit1_accessTimePresent && accessTime != null) {
650             final Date a = getAccessJavaTime();
651             buf.append(" Access:[").append(a).append("] ");
652         }
653         if (bit2_createTimePresent && createTime != null) {
654             final Date c = getCreateJavaTime();
655             buf.append(" Create:[").append(c).append("] ");
656         }
657         return buf.toString();
658     }
659 }