1 | //! Types that specify what is contained in a ZIP. |
2 | use std::path; |
3 | |
4 | #[cfg (not(any( |
5 | all(target_arch = "arm" , target_pointer_width = "32" ), |
6 | target_arch = "mips" , |
7 | target_arch = "powerpc" |
8 | )))] |
9 | use std::sync::atomic; |
10 | #[cfg (not(feature = "time" ))] |
11 | use std::time::SystemTime; |
12 | #[cfg (doc)] |
13 | use {crate::read::ZipFile, crate::write::FileOptions}; |
14 | |
15 | mod ffi { |
16 | pub const S_IFDIR: u32 = 0o0040000; |
17 | pub const S_IFREG: u32 = 0o0100000; |
18 | } |
19 | |
20 | #[cfg (any( |
21 | all(target_arch = "arm" , target_pointer_width = "32" ), |
22 | target_arch = "mips" , |
23 | target_arch = "powerpc" |
24 | ))] |
25 | mod atomic { |
26 | use crossbeam_utils::sync::ShardedLock; |
27 | pub use std::sync::atomic::Ordering; |
28 | |
29 | #[derive (Debug, Default)] |
30 | pub struct AtomicU64 { |
31 | value: ShardedLock<u64>, |
32 | } |
33 | |
34 | impl AtomicU64 { |
35 | pub fn new(v: u64) -> Self { |
36 | Self { |
37 | value: ShardedLock::new(v), |
38 | } |
39 | } |
40 | pub fn get_mut(&mut self) -> &mut u64 { |
41 | self.value.get_mut().unwrap() |
42 | } |
43 | pub fn load(&self, _: Ordering) -> u64 { |
44 | *self.value.read().unwrap() |
45 | } |
46 | pub fn store(&self, value: u64, _: Ordering) { |
47 | *self.value.write().unwrap() = value; |
48 | } |
49 | } |
50 | } |
51 | |
52 | #[cfg (feature = "time" )] |
53 | use crate::result::DateTimeRangeError; |
54 | #[cfg (feature = "time" )] |
55 | use time::{error::ComponentRange, Date, Month, OffsetDateTime, PrimitiveDateTime, Time}; |
56 | |
57 | #[derive (Clone, Copy, Debug, PartialEq, Eq)] |
58 | pub enum System { |
59 | Dos = 0, |
60 | Unix = 3, |
61 | Unknown, |
62 | } |
63 | |
64 | impl System { |
65 | pub fn from_u8(system: u8) -> System { |
66 | use self::System::*; |
67 | |
68 | match system { |
69 | 0 => Dos, |
70 | 3 => Unix, |
71 | _ => Unknown, |
72 | } |
73 | } |
74 | } |
75 | |
76 | /// Representation of a moment in time. |
77 | /// |
78 | /// Zip files use an old format from DOS to store timestamps, |
79 | /// with its own set of peculiarities. |
80 | /// For example, it has a resolution of 2 seconds! |
81 | /// |
82 | /// A [`DateTime`] can be stored directly in a zipfile with [`FileOptions::last_modified_time`], |
83 | /// or read from one with [`ZipFile::last_modified`] |
84 | /// |
85 | /// # Warning |
86 | /// |
87 | /// Because there is no timezone associated with the [`DateTime`], they should ideally only |
88 | /// be used for user-facing descriptions. This also means [`DateTime::to_time`] returns an |
89 | /// [`OffsetDateTime`] (which is the equivalent of chrono's `NaiveDateTime`). |
90 | /// |
91 | /// Modern zip files store more precise timestamps, which are ignored by [`crate::read::ZipArchive`], |
92 | /// so keep in mind that these timestamps are unreliable. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904). |
93 | #[derive (Debug, Clone, Copy)] |
94 | pub struct DateTime { |
95 | year: u16, |
96 | month: u8, |
97 | day: u8, |
98 | hour: u8, |
99 | minute: u8, |
100 | second: u8, |
101 | } |
102 | |
103 | impl ::std::default::Default for DateTime { |
104 | /// Constructs an 'default' datetime of 1980-01-01 00:00:00 |
105 | fn default() -> DateTime { |
106 | DateTime { |
107 | year: 1980, |
108 | month: 1, |
109 | day: 1, |
110 | hour: 0, |
111 | minute: 0, |
112 | second: 0, |
113 | } |
114 | } |
115 | } |
116 | |
117 | impl DateTime { |
118 | /// Converts an msdos (u16, u16) pair to a DateTime object |
119 | pub fn from_msdos(datepart: u16, timepart: u16) -> DateTime { |
120 | let seconds = (timepart & 0b0000000000011111) << 1; |
121 | let minutes = (timepart & 0b0000011111100000) >> 5; |
122 | let hours = (timepart & 0b1111100000000000) >> 11; |
123 | let days = datepart & 0b0000000000011111; |
124 | let months = (datepart & 0b0000000111100000) >> 5; |
125 | let years = (datepart & 0b1111111000000000) >> 9; |
126 | |
127 | DateTime { |
128 | year: years + 1980, |
129 | month: months as u8, |
130 | day: days as u8, |
131 | hour: hours as u8, |
132 | minute: minutes as u8, |
133 | second: seconds as u8, |
134 | } |
135 | } |
136 | |
137 | /// Constructs a DateTime from a specific date and time |
138 | /// |
139 | /// The bounds are: |
140 | /// * year: [1980, 2107] |
141 | /// * month: [1, 12] |
142 | /// * day: [1, 31] |
143 | /// * hour: [0, 23] |
144 | /// * minute: [0, 59] |
145 | /// * second: [0, 60] |
146 | #[allow (clippy::result_unit_err)] |
147 | pub fn from_date_and_time( |
148 | year: u16, |
149 | month: u8, |
150 | day: u8, |
151 | hour: u8, |
152 | minute: u8, |
153 | second: u8, |
154 | ) -> Result<DateTime, ()> { |
155 | if (1980..=2107).contains(&year) |
156 | && (1..=12).contains(&month) |
157 | && (1..=31).contains(&day) |
158 | && hour <= 23 |
159 | && minute <= 59 |
160 | && second <= 60 |
161 | { |
162 | Ok(DateTime { |
163 | year, |
164 | month, |
165 | day, |
166 | hour, |
167 | minute, |
168 | second, |
169 | }) |
170 | } else { |
171 | Err(()) |
172 | } |
173 | } |
174 | |
175 | #[cfg (feature = "time" )] |
176 | /// Converts a OffsetDateTime object to a DateTime |
177 | /// |
178 | /// Returns `Err` when this object is out of bounds |
179 | #[allow (clippy::result_unit_err)] |
180 | #[deprecated (note = "use `DateTime::try_from()`" )] |
181 | pub fn from_time(dt: OffsetDateTime) -> Result<DateTime, ()> { |
182 | dt.try_into().map_err(|_err| ()) |
183 | } |
184 | |
185 | /// Gets the time portion of this datetime in the msdos representation |
186 | pub fn timepart(&self) -> u16 { |
187 | ((self.second as u16) >> 1) | ((self.minute as u16) << 5) | ((self.hour as u16) << 11) |
188 | } |
189 | |
190 | /// Gets the date portion of this datetime in the msdos representation |
191 | pub fn datepart(&self) -> u16 { |
192 | (self.day as u16) | ((self.month as u16) << 5) | ((self.year - 1980) << 9) |
193 | } |
194 | |
195 | #[cfg (feature = "time" )] |
196 | /// Converts the DateTime to a OffsetDateTime structure |
197 | pub fn to_time(&self) -> Result<OffsetDateTime, ComponentRange> { |
198 | let date = |
199 | Date::from_calendar_date(self.year as i32, Month::try_from(self.month)?, self.day)?; |
200 | let time = Time::from_hms(self.hour, self.minute, self.second)?; |
201 | Ok(PrimitiveDateTime::new(date, time).assume_utc()) |
202 | } |
203 | |
204 | /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018. |
205 | pub fn year(&self) -> u16 { |
206 | self.year |
207 | } |
208 | |
209 | /// Get the month, where 1 = january and 12 = december |
210 | /// |
211 | /// # Warning |
212 | /// |
213 | /// When read from a zip file, this may not be a reasonable value |
214 | pub fn month(&self) -> u8 { |
215 | self.month |
216 | } |
217 | |
218 | /// Get the day |
219 | /// |
220 | /// # Warning |
221 | /// |
222 | /// When read from a zip file, this may not be a reasonable value |
223 | pub fn day(&self) -> u8 { |
224 | self.day |
225 | } |
226 | |
227 | /// Get the hour |
228 | /// |
229 | /// # Warning |
230 | /// |
231 | /// When read from a zip file, this may not be a reasonable value |
232 | pub fn hour(&self) -> u8 { |
233 | self.hour |
234 | } |
235 | |
236 | /// Get the minute |
237 | /// |
238 | /// # Warning |
239 | /// |
240 | /// When read from a zip file, this may not be a reasonable value |
241 | pub fn minute(&self) -> u8 { |
242 | self.minute |
243 | } |
244 | |
245 | /// Get the second |
246 | /// |
247 | /// # Warning |
248 | /// |
249 | /// When read from a zip file, this may not be a reasonable value |
250 | pub fn second(&self) -> u8 { |
251 | self.second |
252 | } |
253 | } |
254 | |
255 | #[cfg (feature = "time" )] |
256 | impl TryFrom<OffsetDateTime> for DateTime { |
257 | type Error = DateTimeRangeError; |
258 | |
259 | fn try_from(dt: OffsetDateTime) -> Result<Self, Self::Error> { |
260 | if dt.year() >= 1980 && dt.year() <= 2107 { |
261 | Ok(DateTime { |
262 | year: (dt.year()) as u16, |
263 | month: (dt.month()) as u8, |
264 | day: dt.day(), |
265 | hour: dt.hour(), |
266 | minute: dt.minute(), |
267 | second: dt.second(), |
268 | }) |
269 | } else { |
270 | Err(DateTimeRangeError) |
271 | } |
272 | } |
273 | } |
274 | |
275 | pub const DEFAULT_VERSION: u8 = 46; |
276 | |
277 | /// A type like `AtomicU64` except it implements `Clone` and has predefined |
278 | /// ordering. |
279 | /// |
280 | /// It uses `Relaxed` ordering because it is not used for synchronisation. |
281 | #[derive (Debug)] |
282 | pub struct AtomicU64(atomic::AtomicU64); |
283 | |
284 | impl AtomicU64 { |
285 | pub fn new(v: u64) -> Self { |
286 | Self(atomic::AtomicU64::new(v)) |
287 | } |
288 | |
289 | pub fn load(&self) -> u64 { |
290 | self.0.load(order:atomic::Ordering::Relaxed) |
291 | } |
292 | |
293 | pub fn store(&self, val: u64) { |
294 | self.0.store(val, order:atomic::Ordering::Relaxed) |
295 | } |
296 | |
297 | pub fn get_mut(&mut self) -> &mut u64 { |
298 | self.0.get_mut() |
299 | } |
300 | } |
301 | |
302 | impl Clone for AtomicU64 { |
303 | fn clone(&self) -> Self { |
304 | Self(atomic::AtomicU64::new(self.load())) |
305 | } |
306 | } |
307 | |
308 | /// Structure representing a ZIP file. |
309 | #[derive (Debug, Clone)] |
310 | pub struct ZipFileData { |
311 | /// Compatibility of the file attribute information |
312 | pub system: System, |
313 | /// Specification version |
314 | pub version_made_by: u8, |
315 | /// True if the file is encrypted. |
316 | pub encrypted: bool, |
317 | /// True if the file uses a data-descriptor section |
318 | pub using_data_descriptor: bool, |
319 | /// Compression method used to store the file |
320 | pub compression_method: crate::compression::CompressionMethod, |
321 | /// Compression level to store the file |
322 | pub compression_level: Option<i32>, |
323 | /// Last modified time. This will only have a 2 second precision. |
324 | pub last_modified_time: DateTime, |
325 | /// CRC32 checksum |
326 | pub crc32: u32, |
327 | /// Size of the file in the ZIP |
328 | pub compressed_size: u64, |
329 | /// Size of the file when extracted |
330 | pub uncompressed_size: u64, |
331 | /// Name of the file |
332 | pub file_name: String, |
333 | /// Raw file name. To be used when file_name was incorrectly decoded. |
334 | pub file_name_raw: Vec<u8>, |
335 | /// Extra field usually used for storage expansion |
336 | pub extra_field: Vec<u8>, |
337 | /// File comment |
338 | pub file_comment: String, |
339 | /// Specifies where the local header of the file starts |
340 | pub header_start: u64, |
341 | /// Specifies where the central header of the file starts |
342 | /// |
343 | /// Note that when this is not known, it is set to 0 |
344 | pub central_header_start: u64, |
345 | /// Specifies where the compressed data of the file starts |
346 | pub data_start: AtomicU64, |
347 | /// External file attributes |
348 | pub external_attributes: u32, |
349 | /// Reserve local ZIP64 extra field |
350 | pub large_file: bool, |
351 | /// AES mode if applicable |
352 | pub aes_mode: Option<(AesMode, AesVendorVersion)>, |
353 | } |
354 | |
355 | impl ZipFileData { |
356 | pub fn file_name_sanitized(&self) -> ::std::path::PathBuf { |
357 | let no_null_filename = match self.file_name.find(' \0' ) { |
358 | Some(index) => &self.file_name[0..index], |
359 | None => &self.file_name, |
360 | } |
361 | .to_string(); |
362 | |
363 | // zip files can contain both / and \ as separators regardless of the OS |
364 | // and as we want to return a sanitized PathBuf that only supports the |
365 | // OS separator let's convert incompatible separators to compatible ones |
366 | let separator = ::std::path::MAIN_SEPARATOR; |
367 | let opposite_separator = match separator { |
368 | '/' => ' \\' , |
369 | _ => '/' , |
370 | }; |
371 | let filename = |
372 | no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string()); |
373 | |
374 | ::std::path::Path::new(&filename) |
375 | .components() |
376 | .filter(|component| matches!(*component, ::std::path::Component::Normal(..))) |
377 | .fold(::std::path::PathBuf::new(), |mut path, ref cur| { |
378 | path.push(cur.as_os_str()); |
379 | path |
380 | }) |
381 | } |
382 | |
383 | pub(crate) fn enclosed_name(&self) -> Option<&path::Path> { |
384 | if self.file_name.contains(' \0' ) { |
385 | return None; |
386 | } |
387 | let path = path::Path::new(&self.file_name); |
388 | let mut depth = 0usize; |
389 | for component in path.components() { |
390 | match component { |
391 | path::Component::Prefix(_) | path::Component::RootDir => return None, |
392 | path::Component::ParentDir => depth = depth.checked_sub(1)?, |
393 | path::Component::Normal(_) => depth += 1, |
394 | path::Component::CurDir => (), |
395 | } |
396 | } |
397 | Some(path) |
398 | } |
399 | |
400 | /// Get unix mode for the file |
401 | pub(crate) fn unix_mode(&self) -> Option<u32> { |
402 | if self.external_attributes == 0 { |
403 | return None; |
404 | } |
405 | |
406 | match self.system { |
407 | System::Unix => Some(self.external_attributes >> 16), |
408 | System::Dos => { |
409 | // Interpret MS-DOS directory bit |
410 | let mut mode = if 0x10 == (self.external_attributes & 0x10) { |
411 | ffi::S_IFDIR | 0o0775 |
412 | } else { |
413 | ffi::S_IFREG | 0o0664 |
414 | }; |
415 | if 0x01 == (self.external_attributes & 0x01) { |
416 | // Read-only bit; strip write permissions |
417 | mode &= 0o0555; |
418 | } |
419 | Some(mode) |
420 | } |
421 | _ => None, |
422 | } |
423 | } |
424 | |
425 | pub fn zip64_extension(&self) -> bool { |
426 | self.uncompressed_size > 0xFFFFFFFF |
427 | || self.compressed_size > 0xFFFFFFFF |
428 | || self.header_start > 0xFFFFFFFF |
429 | } |
430 | |
431 | pub fn version_needed(&self) -> u16 { |
432 | // higher versions matched first |
433 | match (self.zip64_extension(), self.compression_method) { |
434 | #[cfg (feature = "bzip2" )] |
435 | (_, crate::compression::CompressionMethod::Bzip2) => 46, |
436 | (true, _) => 45, |
437 | _ => 20, |
438 | } |
439 | } |
440 | } |
441 | |
442 | /// The encryption specification used to encrypt a file with AES. |
443 | /// |
444 | /// According to the [specification](https://www.winzip.com/win/en/aes_info.html#winzip11) AE-2 |
445 | /// does not make use of the CRC check. |
446 | #[derive (Copy, Clone, Debug)] |
447 | pub enum AesVendorVersion { |
448 | Ae1, |
449 | Ae2, |
450 | } |
451 | |
452 | /// AES variant used. |
453 | #[derive (Copy, Clone, Debug)] |
454 | pub enum AesMode { |
455 | Aes128, |
456 | Aes192, |
457 | Aes256, |
458 | } |
459 | |
460 | #[cfg (feature = "aes-crypto" )] |
461 | impl AesMode { |
462 | pub fn salt_length(&self) -> usize { |
463 | self.key_length() / 2 |
464 | } |
465 | |
466 | pub fn key_length(&self) -> usize { |
467 | match self { |
468 | Self::Aes128 => 16, |
469 | Self::Aes192 => 24, |
470 | Self::Aes256 => 32, |
471 | } |
472 | } |
473 | } |
474 | |
475 | #[cfg (test)] |
476 | mod test { |
477 | #[test ] |
478 | fn system() { |
479 | use super::System; |
480 | assert_eq!(System::Dos as u16, 0u16); |
481 | assert_eq!(System::Unix as u16, 3u16); |
482 | assert_eq!(System::from_u8(0), System::Dos); |
483 | assert_eq!(System::from_u8(3), System::Unix); |
484 | } |
485 | |
486 | #[test ] |
487 | fn sanitize() { |
488 | use super::*; |
489 | let file_name = "/path/../../../../etc/./passwd \0/etc/shadow" .to_string(); |
490 | let data = ZipFileData { |
491 | system: System::Dos, |
492 | version_made_by: 0, |
493 | encrypted: false, |
494 | using_data_descriptor: false, |
495 | compression_method: crate::compression::CompressionMethod::Stored, |
496 | compression_level: None, |
497 | last_modified_time: DateTime::default(), |
498 | crc32: 0, |
499 | compressed_size: 0, |
500 | uncompressed_size: 0, |
501 | file_name: file_name.clone(), |
502 | file_name_raw: file_name.into_bytes(), |
503 | extra_field: Vec::new(), |
504 | file_comment: String::new(), |
505 | header_start: 0, |
506 | data_start: AtomicU64::new(0), |
507 | central_header_start: 0, |
508 | external_attributes: 0, |
509 | large_file: false, |
510 | aes_mode: None, |
511 | }; |
512 | assert_eq!( |
513 | data.file_name_sanitized(), |
514 | ::std::path::PathBuf::from("path/etc/passwd" ) |
515 | ); |
516 | } |
517 | |
518 | #[test ] |
519 | #[allow (clippy::unusual_byte_groupings)] |
520 | fn datetime_default() { |
521 | use super::DateTime; |
522 | let dt = DateTime::default(); |
523 | assert_eq!(dt.timepart(), 0); |
524 | assert_eq!(dt.datepart(), 0b0000000_0001_00001); |
525 | } |
526 | |
527 | #[test ] |
528 | #[allow (clippy::unusual_byte_groupings)] |
529 | fn datetime_max() { |
530 | use super::DateTime; |
531 | let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap(); |
532 | assert_eq!(dt.timepart(), 0b10111_111011_11110); |
533 | assert_eq!(dt.datepart(), 0b1111111_1100_11111); |
534 | } |
535 | |
536 | #[test ] |
537 | fn datetime_bounds() { |
538 | use super::DateTime; |
539 | |
540 | assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok()); |
541 | assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err()); |
542 | assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err()); |
543 | assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err()); |
544 | |
545 | assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok()); |
546 | assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok()); |
547 | assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err()); |
548 | assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err()); |
549 | assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err()); |
550 | assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err()); |
551 | assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err()); |
552 | assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err()); |
553 | } |
554 | |
555 | #[cfg (feature = "time" )] |
556 | use time::{format_description::well_known::Rfc3339, OffsetDateTime}; |
557 | |
558 | #[cfg (feature = "time" )] |
559 | #[test ] |
560 | fn datetime_try_from_bounds() { |
561 | use std::convert::TryFrom; |
562 | |
563 | use super::DateTime; |
564 | use time::macros::datetime; |
565 | |
566 | // 1979-12-31 23:59:59 |
567 | assert!(DateTime::try_from(datetime!(1979-12-31 23:59:59 UTC)).is_err()); |
568 | |
569 | // 1980-01-01 00:00:00 |
570 | assert!(DateTime::try_from(datetime!(1980-01-01 00:00:00 UTC)).is_ok()); |
571 | |
572 | // 2107-12-31 23:59:59 |
573 | assert!(DateTime::try_from(datetime!(2107-12-31 23:59:59 UTC)).is_ok()); |
574 | |
575 | // 2108-01-01 00:00:00 |
576 | assert!(DateTime::try_from(datetime!(2108-01-01 00:00:00 UTC)).is_err()); |
577 | } |
578 | |
579 | #[test ] |
580 | fn time_conversion() { |
581 | use super::DateTime; |
582 | let dt = DateTime::from_msdos(0x4D71, 0x54CF); |
583 | assert_eq!(dt.year(), 2018); |
584 | assert_eq!(dt.month(), 11); |
585 | assert_eq!(dt.day(), 17); |
586 | assert_eq!(dt.hour(), 10); |
587 | assert_eq!(dt.minute(), 38); |
588 | assert_eq!(dt.second(), 30); |
589 | |
590 | #[cfg (feature = "time" )] |
591 | assert_eq!( |
592 | dt.to_time().unwrap().format(&Rfc3339).unwrap(), |
593 | "2018-11-17T10:38:30Z" |
594 | ); |
595 | } |
596 | |
597 | #[test ] |
598 | fn time_out_of_bounds() { |
599 | use super::DateTime; |
600 | let dt = DateTime::from_msdos(0xFFFF, 0xFFFF); |
601 | assert_eq!(dt.year(), 2107); |
602 | assert_eq!(dt.month(), 15); |
603 | assert_eq!(dt.day(), 31); |
604 | assert_eq!(dt.hour(), 31); |
605 | assert_eq!(dt.minute(), 63); |
606 | assert_eq!(dt.second(), 62); |
607 | |
608 | #[cfg (feature = "time" )] |
609 | assert!(dt.to_time().is_err()); |
610 | |
611 | let dt = DateTime::from_msdos(0x0000, 0x0000); |
612 | assert_eq!(dt.year(), 1980); |
613 | assert_eq!(dt.month(), 0); |
614 | assert_eq!(dt.day(), 0); |
615 | assert_eq!(dt.hour(), 0); |
616 | assert_eq!(dt.minute(), 0); |
617 | assert_eq!(dt.second(), 0); |
618 | |
619 | #[cfg (feature = "time" )] |
620 | assert!(dt.to_time().is_err()); |
621 | } |
622 | |
623 | #[cfg (feature = "time" )] |
624 | #[test ] |
625 | fn time_at_january() { |
626 | use super::DateTime; |
627 | use std::convert::TryFrom; |
628 | |
629 | // 2020-01-01 00:00:00 |
630 | let clock = OffsetDateTime::from_unix_timestamp(1_577_836_800).unwrap(); |
631 | |
632 | assert!(DateTime::try_from(clock).is_ok()); |
633 | } |
634 | } |
635 | |