1#[cfg(all(not(feature = "std"), feature = "alloc"))]
2use alloc::{string::String, vec::Vec};
3use core::char;
4use core::iter::FromIterator;
5use core::{fmt, str};
6use io;
7use io::prelude::*;
8use io::Cursor;
9
10use byteorder::LittleEndian;
11use byteorder_ext::{ReadBytesExt, WriteBytesExt};
12
13use dir::{Dir, DirRawStream};
14use file::File;
15use fs::{FatType, FileSystem, OemCpConverter, ReadWriteSeek};
16use time::{Date, DateTime};
17
18bitflags! {
19 #[derive(Default)]
21 pub struct FileAttributes: u8 {
22 const READ_ONLY = 0x01;
23 const HIDDEN = 0x02;
24 const SYSTEM = 0x04;
25 const VOLUME_ID = 0x08;
26 const DIRECTORY = 0x10;
27 const ARCHIVE = 0x20;
28 const LFN = Self::READ_ONLY.bits | Self::HIDDEN.bits
29 | Self::SYSTEM.bits | Self::VOLUME_ID.bits;
30 }
31}
32
33pub(crate) const DIR_ENTRY_SIZE: u64 = 32;
35
36pub(crate) const DIR_ENTRY_DELETED_FLAG: u8 = 0xE5;
38pub(crate) const DIR_ENTRY_REALLY_E5_FLAG: u8 = 0x05;
39
40pub(crate) const LFN_PART_LEN: usize = 13;
42
43pub(crate) const LFN_ENTRY_LAST_FLAG: u8 = 0x40;
45
46#[derive(Clone, Debug, Default)]
48pub(crate) struct ShortName {
49 name: [u8; 12],
50 len: u8,
51}
52
53impl ShortName {
54 const PADDING: u8 = b' ';
55
56 pub(crate) fn new(raw_name: &[u8; 11]) -> Self {
57 let name_len = raw_name[0..8].iter().rposition(|x| *x != Self::PADDING).map(|p| p + 1).unwrap_or(0);
59 let ext_len = raw_name[8..11].iter().rposition(|x| *x != Self::PADDING).map(|p| p + 1).unwrap_or(0);
60 let mut name = [Self::PADDING; 12];
61 name[..name_len].copy_from_slice(&raw_name[..name_len]);
62 let total_len = if ext_len > 0 {
63 name[name_len] = b'.';
64 name[name_len + 1..name_len + 1 + ext_len].copy_from_slice(&raw_name[8..8 + ext_len]);
65 name_len + 1 + ext_len
67 } else {
68 name_len
70 };
71 if name[0] == DIR_ENTRY_REALLY_E5_FLAG {
73 name[0] = 0xE5;
74 }
75 ShortName { name, len: total_len as u8 }
77 }
78
79 fn as_bytes(&self) -> &[u8] {
80 &self.name[..self.len as usize]
81 }
82
83 #[cfg(feature = "alloc")]
84 fn to_string(&self, oem_cp_converter: &OemCpConverter) -> String {
85 let char_iter = self.as_bytes().iter().cloned().map(|c| oem_cp_converter.decode(c));
87 String::from_iter(char_iter)
89 }
90
91 fn eq_ignore_case(&self, name: &str, oem_cp_converter: &OemCpConverter) -> bool {
92 let byte_iter = self.as_bytes().iter().cloned();
94 let char_iter = byte_iter.map(|c| oem_cp_converter.decode(c));
95 let uppercase_char_iter = char_iter.flat_map(|c| c.to_uppercase());
96 uppercase_char_iter.eq(name.chars().flat_map(|c| c.to_uppercase()))
98 }
99}
100
101#[allow(dead_code)]
102#[derive(Clone, Debug, Default)]
103pub(crate) struct DirFileEntryData {
104 name: [u8; 11],
105 attrs: FileAttributes,
106 reserved_0: u8,
107 create_time_0: u8,
108 create_time_1: u16,
109 create_date: u16,
110 access_date: u16,
111 first_cluster_hi: u16,
112 modify_time: u16,
113 modify_date: u16,
114 first_cluster_lo: u16,
115 size: u32,
116}
117
118impl DirFileEntryData {
119 pub(crate) fn new(name: [u8; 11], attrs: FileAttributes) -> Self {
120 DirFileEntryData { name, attrs, ..Default::default() }
121 }
122
123 pub(crate) fn renamed(&self, new_name: [u8; 11]) -> Self {
124 let mut sfn_entry = self.clone();
125 sfn_entry.name = new_name;
126 sfn_entry
127 }
128
129 pub(crate) fn name(&self) -> &[u8; 11] {
130 &self.name
131 }
132
133 #[cfg(feature = "alloc")]
134 fn lowercase_name(&self) -> ShortName {
135 let mut name_copy: [u8; 11] = self.name;
136 if self.lowercase_basename() {
137 for c in &mut name_copy[..8] {
138 *c = (*c as char).to_ascii_lowercase() as u8;
139 }
140 }
141 if self.lowercase_ext() {
142 for c in &mut name_copy[8..] {
143 *c = (*c as char).to_ascii_lowercase() as u8;
144 }
145 }
146 ShortName::new(&name_copy)
147 }
148
149 pub(crate) fn first_cluster(&self, fat_type: FatType) -> Option<u32> {
150 let first_cluster_hi = if fat_type == FatType::Fat32 { self.first_cluster_hi } else { 0 };
151 let n = ((first_cluster_hi as u32) << 16) | self.first_cluster_lo as u32;
152 if n == 0 {
153 None
154 } else {
155 Some(n)
156 }
157 }
158
159 pub(crate) fn set_first_cluster(&mut self, cluster: Option<u32>, fat_type: FatType) {
160 let n = cluster.unwrap_or(0);
161 if fat_type == FatType::Fat32 {
162 self.first_cluster_hi = (n >> 16) as u16;
163 }
164 self.first_cluster_lo = (n & 0xFFFF) as u16;
165 }
166
167 pub(crate) fn size(&self) -> Option<u32> {
168 if self.is_file() {
169 Some(self.size)
170 } else {
171 None
172 }
173 }
174
175 fn set_size(&mut self, size: u32) {
176 self.size = size;
177 }
178
179 pub(crate) fn is_dir(&self) -> bool {
180 self.attrs.contains(FileAttributes::DIRECTORY)
181 }
182
183 fn is_file(&self) -> bool {
184 !self.is_dir()
185 }
186
187 fn lowercase_basename(&self) -> bool {
188 self.reserved_0 & (1 << 3) != 0
189 }
190
191 fn lowercase_ext(&self) -> bool {
192 self.reserved_0 & (1 << 4) != 0
193 }
194
195 fn created(&self) -> DateTime {
196 DateTime::decode(self.create_date, self.create_time_1, self.create_time_0)
197 }
198
199 fn accessed(&self) -> Date {
200 Date::decode(self.access_date)
201 }
202
203 fn modified(&self) -> DateTime {
204 DateTime::decode(self.modify_date, self.modify_time, 0)
205 }
206
207 pub(crate) fn set_created(&mut self, date_time: DateTime) {
208 self.create_date = date_time.date.encode();
209 let encoded_time = date_time.time.encode();
210 self.create_time_1 = encoded_time.0;
211 self.create_time_0 = encoded_time.1;
212 }
213
214 pub(crate) fn set_accessed(&mut self, date: Date) {
215 self.access_date = date.encode();
216 }
217
218 pub(crate) fn set_modified(&mut self, date_time: DateTime) {
219 self.modify_date = date_time.date.encode();
220 self.modify_time = date_time.time.encode().0;
221 }
222
223 pub(crate) fn serialize(&self, wrt: &mut Write) -> io::Result<()> {
224 wrt.write_all(&self.name)?;
225 wrt.write_u8(self.attrs.bits())?;
226 wrt.write_u8(self.reserved_0)?;
227 wrt.write_u8(self.create_time_0)?;
228 wrt.write_u16::<LittleEndian>(self.create_time_1)?;
229 wrt.write_u16::<LittleEndian>(self.create_date)?;
230 wrt.write_u16::<LittleEndian>(self.access_date)?;
231 wrt.write_u16::<LittleEndian>(self.first_cluster_hi)?;
232 wrt.write_u16::<LittleEndian>(self.modify_time)?;
233 wrt.write_u16::<LittleEndian>(self.modify_date)?;
234 wrt.write_u16::<LittleEndian>(self.first_cluster_lo)?;
235 wrt.write_u32::<LittleEndian>(self.size)?;
236 Ok(())
237 }
238
239 pub(crate) fn is_deleted(&self) -> bool {
240 self.name[0] == DIR_ENTRY_DELETED_FLAG
241 }
242
243 pub(crate) fn set_deleted(&mut self) {
244 self.name[0] = DIR_ENTRY_DELETED_FLAG;
245 }
246
247 pub(crate) fn is_end(&self) -> bool {
248 self.name[0] == 0
249 }
250
251 pub(crate) fn is_volume(&self) -> bool {
252 self.attrs.contains(FileAttributes::VOLUME_ID)
253 }
254}
255
256#[allow(dead_code)]
257#[derive(Clone, Debug, Default)]
258pub(crate) struct DirLfnEntryData {
259 order: u8,
260 name_0: [u16; 5],
261 attrs: FileAttributes,
262 entry_type: u8,
263 checksum: u8,
264 name_1: [u16; 6],
265 reserved_0: u16,
266 name_2: [u16; 2],
267}
268
269impl DirLfnEntryData {
270 pub(crate) fn new(order: u8, checksum: u8) -> Self {
271 DirLfnEntryData { order, checksum, attrs: FileAttributes::LFN, ..Default::default() }
272 }
273
274 pub(crate) fn copy_name_from_slice(&mut self, lfn_part: &[u16; LFN_PART_LEN]) {
275 self.name_0.copy_from_slice(&lfn_part[0..5]);
276 self.name_1.copy_from_slice(&lfn_part[5..5 + 6]);
277 self.name_2.copy_from_slice(&lfn_part[11..11 + 2]);
278 }
279
280 pub(crate) fn copy_name_to_slice(&self, lfn_part: &mut [u16]) {
281 debug_assert!(lfn_part.len() == LFN_PART_LEN);
282 lfn_part[0..5].copy_from_slice(&self.name_0);
283 lfn_part[5..11].copy_from_slice(&self.name_1);
284 lfn_part[11..13].copy_from_slice(&self.name_2);
285 }
286
287 pub(crate) fn serialize(&self, wrt: &mut Write) -> io::Result<()> {
288 wrt.write_u8(self.order)?;
289 for ch in self.name_0.iter() {
290 wrt.write_u16::<LittleEndian>(*ch)?;
291 }
292 wrt.write_u8(self.attrs.bits())?;
293 wrt.write_u8(self.entry_type)?;
294 wrt.write_u8(self.checksum)?;
295 for ch in self.name_1.iter() {
296 wrt.write_u16::<LittleEndian>(*ch)?;
297 }
298 wrt.write_u16::<LittleEndian>(self.reserved_0)?;
299 for ch in self.name_2.iter() {
300 wrt.write_u16::<LittleEndian>(*ch)?;
301 }
302 Ok(())
303 }
304
305 #[cfg(feature = "alloc")]
306 pub(crate) fn order(&self) -> u8 {
307 self.order
308 }
309
310 #[cfg(feature = "alloc")]
311 pub(crate) fn checksum(&self) -> u8 {
312 self.checksum
313 }
314
315 pub(crate) fn is_deleted(&self) -> bool {
316 self.order == DIR_ENTRY_DELETED_FLAG
317 }
318
319 pub(crate) fn set_deleted(&mut self) {
320 self.order = DIR_ENTRY_DELETED_FLAG;
321 }
322
323 pub(crate) fn is_end(&self) -> bool {
324 self.order == 0
325 }
326}
327
328#[derive(Clone, Debug)]
329pub(crate) enum DirEntryData {
330 File(DirFileEntryData),
331 Lfn(DirLfnEntryData),
332}
333
334impl DirEntryData {
335 pub(crate) fn serialize(&self, wrt: &mut Write) -> io::Result<()> {
336 match self {
337 &DirEntryData::File(ref file) => file.serialize(wrt),
338 &DirEntryData::Lfn(ref lfn) => lfn.serialize(wrt),
339 }
340 }
341
342 pub(crate) fn deserialize(rdr: &mut Read) -> io::Result<Self> {
343 let mut name = [0; 11];
344 match rdr.read_exact(&mut name) {
345 Err(ref err) if err.kind() == io::ErrorKind::UnexpectedEof => {
346 return Ok(DirEntryData::File(DirFileEntryData { ..Default::default() }));
349 },
350 Err(err) => return Err(err),
351 _ => {},
352 }
353 let attrs = FileAttributes::from_bits_truncate(rdr.read_u8()?);
354 if attrs & FileAttributes::LFN == FileAttributes::LFN {
355 let mut data = DirLfnEntryData { attrs, ..Default::default() };
357 let mut cur = Cursor::new(&name);
359 data.order = cur.read_u8()?;
360 cur.read_u16_into::<LittleEndian>(&mut data.name_0)?;
361 data.entry_type = rdr.read_u8()?;
362 data.checksum = rdr.read_u8()?;
363 rdr.read_u16_into::<LittleEndian>(&mut data.name_1)?;
364 data.reserved_0 = rdr.read_u16::<LittleEndian>()?;
365 rdr.read_u16_into::<LittleEndian>(&mut data.name_2)?;
366 Ok(DirEntryData::Lfn(data))
367 } else {
368 let data = DirFileEntryData {
370 name,
371 attrs,
372 reserved_0: rdr.read_u8()?,
373 create_time_0: rdr.read_u8()?,
374 create_time_1: rdr.read_u16::<LittleEndian>()?,
375 create_date: rdr.read_u16::<LittleEndian>()?,
376 access_date: rdr.read_u16::<LittleEndian>()?,
377 first_cluster_hi: rdr.read_u16::<LittleEndian>()?,
378 modify_time: rdr.read_u16::<LittleEndian>()?,
379 modify_date: rdr.read_u16::<LittleEndian>()?,
380 first_cluster_lo: rdr.read_u16::<LittleEndian>()?,
381 size: rdr.read_u32::<LittleEndian>()?,
382 };
383 Ok(DirEntryData::File(data))
384 }
385 }
386
387 pub(crate) fn is_deleted(&self) -> bool {
388 match self {
389 &DirEntryData::File(ref file) => file.is_deleted(),
390 &DirEntryData::Lfn(ref lfn) => lfn.is_deleted(),
391 }
392 }
393
394 pub(crate) fn set_deleted(&mut self) {
395 match self {
396 &mut DirEntryData::File(ref mut file) => file.set_deleted(),
397 &mut DirEntryData::Lfn(ref mut lfn) => lfn.set_deleted(),
398 }
399 }
400
401 pub(crate) fn is_end(&self) -> bool {
402 match self {
403 &DirEntryData::File(ref file) => file.is_end(),
404 &DirEntryData::Lfn(ref lfn) => lfn.is_end(),
405 }
406 }
407}
408
409#[derive(Clone, Debug)]
410pub(crate) struct DirEntryEditor {
411 data: DirFileEntryData,
412 pos: u64,
413 dirty: bool,
414}
415
416impl DirEntryEditor {
417 fn new(data: DirFileEntryData, pos: u64) -> Self {
418 DirEntryEditor { data, pos, dirty: false }
419 }
420
421 pub(crate) fn inner(&self) -> &DirFileEntryData {
422 &self.data
423 }
424
425 pub(crate) fn set_first_cluster(&mut self, first_cluster: Option<u32>, fat_type: FatType) {
426 if first_cluster != self.data.first_cluster(fat_type) {
427 self.data.set_first_cluster(first_cluster, fat_type);
428 self.dirty = true;
429 }
430 }
431
432 pub(crate) fn set_size(&mut self, size: u32) {
433 match self.data.size() {
434 Some(n) if size != n => {
435 self.data.set_size(size);
436 self.dirty = true;
437 },
438 _ => {},
439 }
440 }
441
442 pub(crate) fn set_created(&mut self, date_time: DateTime) {
443 if date_time != self.data.created() {
444 self.data.set_created(date_time);
445 self.dirty = true;
446 }
447 }
448
449 pub(crate) fn set_accessed(&mut self, date: Date) {
450 if date != self.data.accessed() {
451 self.data.set_accessed(date);
452 self.dirty = true;
453 }
454 }
455
456 pub(crate) fn set_modified(&mut self, date_time: DateTime) {
457 if date_time != self.data.modified() {
458 self.data.set_modified(date_time);
459 self.dirty = true;
460 }
461 }
462
463 pub(crate) fn flush<T: ReadWriteSeek>(&mut self, fs: &FileSystem<T>) -> io::Result<()> {
464 if self.dirty {
465 self.write(fs)?;
466 self.dirty = false;
467 }
468 Ok(())
469 }
470
471 fn write<T: ReadWriteSeek>(&self, fs: &FileSystem<T>) -> io::Result<()> {
472 let mut disk = fs.disk.borrow_mut();
473 disk.seek(io::SeekFrom::Start(self.pos))?;
474 self.data.serialize(&mut *disk)
475 }
476}
477
478#[derive(Clone)]
482pub struct DirEntry<'a, T: ReadWriteSeek + 'a> {
483 pub(crate) data: DirFileEntryData,
484 pub(crate) short_name: ShortName,
485 #[cfg(feature = "alloc")]
486 pub(crate) lfn_utf16: Vec<u16>,
487 #[cfg(not(feature = "alloc"))]
488 pub(crate) lfn_utf16: (),
489 pub(crate) entry_pos: u64,
490 pub(crate) offset_range: (u64, u64),
491 pub(crate) fs: &'a FileSystem<T>,
492}
493
494impl<'a, T: ReadWriteSeek> DirEntry<'a, T> {
495 #[cfg(feature = "alloc")]
499 pub fn short_file_name(&self) -> String {
500 self.short_name.to_string(self.fs.options.oem_cp_converter)
501 }
502
503 pub fn short_file_name_as_bytes(&self) -> &[u8] {
507 self.short_name.as_bytes()
508 }
509
510 #[cfg(feature = "alloc")]
512 pub fn file_name(&self) -> String {
513 if self.lfn_utf16.is_empty() {
514 self.data.lowercase_name().to_string(self.fs.options.oem_cp_converter)
515 } else {
516 String::from_utf16_lossy(&self.lfn_utf16)
517 }
518 }
519
520 pub fn attributes(&self) -> FileAttributes {
522 self.data.attrs
523 }
524
525 pub fn is_dir(&self) -> bool {
527 self.data.is_dir()
528 }
529
530 pub fn is_file(&self) -> bool {
532 self.data.is_file()
533 }
534
535 pub(crate) fn first_cluster(&self) -> Option<u32> {
536 self.data.first_cluster(self.fs.fat_type())
537 }
538
539 fn editor(&self) -> DirEntryEditor {
540 DirEntryEditor::new(self.data.clone(), self.entry_pos)
541 }
542
543 pub(crate) fn is_same_entry(&self, other: &DirEntry<T>) -> bool {
544 self.entry_pos == other.entry_pos
545 }
546
547 pub fn to_file(&self) -> File<'a, T> {
551 assert!(!self.is_dir(), "Not a file entry");
552 File::new(self.first_cluster(), Some(self.editor()), self.fs)
553 }
554
555 pub fn to_dir(&self) -> Dir<'a, T> {
559 assert!(self.is_dir(), "Not a directory entry");
560 match self.first_cluster() {
561 Some(n) => {
562 let file = File::new(Some(n), Some(self.editor()), self.fs);
563 Dir::new(DirRawStream::File(file), self.fs)
564 },
565 None => self.fs.root_dir(),
566 }
567 }
568
569 pub fn len(&self) -> u64 {
571 self.data.size as u64
572 }
573
574 pub fn created(&self) -> DateTime {
578 self.data.created()
579 }
580
581 pub fn accessed(&self) -> Date {
583 self.data.accessed()
584 }
585
586 pub fn modified(&self) -> DateTime {
590 self.data.modified()
591 }
592
593 pub(crate) fn raw_short_name(&self) -> &[u8; 11] {
594 &self.data.name
595 }
596
597 #[cfg(feature = "alloc")]
598 pub(crate) fn eq_name(&self, name: &str) -> bool {
599 let self_name = self.file_name();
600 let self_name_lowercase_iter = self_name.chars().flat_map(|c| c.to_uppercase());
601 let other_name_lowercase_iter = name.chars().flat_map(|c| c.to_uppercase());
602 let long_name_matches = self_name_lowercase_iter.eq(other_name_lowercase_iter);
603 let short_name_matches = self.short_name.eq_ignore_case(name, self.fs.options.oem_cp_converter);
604 long_name_matches || short_name_matches
605 }
606 #[cfg(not(feature = "alloc"))]
607 pub(crate) fn eq_name(&self, name: &str) -> bool {
608 self.short_name.eq_ignore_case(name, self.fs.options.oem_cp_converter)
609 }
610}
611
612impl<'a, T: ReadWriteSeek> fmt::Debug for DirEntry<'a, T> {
613 fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
614 self.data.fmt(f)
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use fs::LOSSY_OEM_CP_CONVERTER;
622
623 #[test]
624 fn short_name_with_ext() {
625 let mut raw_short_name = [0u8; 11];
626 raw_short_name.copy_from_slice(b"FOO BAR");
627 assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "FOO.BAR");
628 raw_short_name.copy_from_slice(b"LOOK AT M E");
629 assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "LOOK AT.M E");
630 raw_short_name[0] = 0x99;
631 raw_short_name[10] = 0x99;
632 assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "\u{FFFD}OOK AT.M \u{FFFD}");
633 assert_eq!(
634 ShortName::new(&raw_short_name).eq_ignore_case("\u{FFFD}OOK AT.M \u{FFFD}", &LOSSY_OEM_CP_CONVERTER),
635 true
636 );
637 }
638
639 #[test]
640 fn short_name_without_ext() {
641 let mut raw_short_name = [0u8; 11];
642 raw_short_name.copy_from_slice(b"FOO ");
643 assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "FOO");
644 raw_short_name.copy_from_slice(b"LOOK AT ");
645 assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "LOOK AT");
646 }
647
648 #[test]
649 fn short_name_eq_ignore_case() {
650 let mut raw_short_name = [0u8; 11];
651 raw_short_name.copy_from_slice(b"LOOK AT M E");
652 raw_short_name[0] = 0x99;
653 raw_short_name[10] = 0x99;
654 assert_eq!(
655 ShortName::new(&raw_short_name).eq_ignore_case("\u{FFFD}OOK AT.M \u{FFFD}", &LOSSY_OEM_CP_CONVERTER),
656 true
657 );
658 assert_eq!(
659 ShortName::new(&raw_short_name).eq_ignore_case("\u{FFFD}ook AT.m \u{FFFD}", &LOSSY_OEM_CP_CONVERTER),
660 true
661 );
662 }
663
664 #[test]
665 fn short_name_05_changed_to_e5() {
666 let raw_short_name = [0x05; 11];
667 assert_eq!(
668 ShortName::new(&raw_short_name).as_bytes(),
669 [0xE5, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, b'.', 0x05, 0x05, 0x05]
670 );
671 }
672
673 #[test]
674 fn lowercase_short_name() {
675 let mut raw_short_name = [0u8; 11];
676 raw_short_name.copy_from_slice(b"FOO RS ");
677 let mut raw_entry =
678 DirFileEntryData { name: raw_short_name, reserved_0: (1 << 3) | (1 << 4), ..Default::default() };
679 assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "foo.rs");
680 raw_entry.reserved_0 = 1 << 3;
681 assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "foo.RS");
682 raw_entry.reserved_0 = 1 << 4;
683 assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "FOO.rs");
684 raw_entry.reserved_0 = 0;
685 assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "FOO.RS");
686 }
687}