Skip to main content

rars_format/rar50/write/
mod.rs

1use super::*;
2pub use rars_codec::rar50::Rar50FilterKind as FilterKind;
3use rars_codec::rar50::Unpack50Encoder;
4use rars_crypto::rar50::{Rar50Cipher, Rar50Keys};
5use rars_recovery::rar5::build_structural_inline_recovery_data;
6
7mod filter_policy;
8mod volume;
9#[cfg(test)]
10use filter_policy::encode_member_with_filter_policy;
11use filter_policy::{
12    compression_info, compression_method_for_level, dictionary_size_for_options,
13    encode_member_with_filter_policy_candidates, encode_option_candidates_for_level,
14    encode_options_for_level, encode_safe_lz_member, encode_with_solid_reset_policy,
15    rar50_algorithm_version, should_store_compressed_payload, validate_compression_level,
16};
17use volume::{
18    write_compressed_volume_set_impl, write_encrypted_compressed_volume_set_impl,
19    write_encrypted_stored_volumes_impl, write_stored_volumes_impl,
20};
21
22const MAX_MATCH_CANDIDATES_DEFAULT: usize = 256;
23const DEFAULT_RAR50_DICTIONARY_SIZE: u64 = 128 * 1024;
24const AUTO_DELTA_EDGE_SKIP: usize = 64;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[non_exhaustive]
28pub struct WriterOptions {
29    pub target: crate::ArchiveVersion,
30    pub features: crate::FeatureSet,
31    pub compression_level: Option<u8>,
32    pub dictionary_size: Option<u64>,
33}
34
35impl WriterOptions {
36    pub const fn new(target: crate::ArchiveVersion, features: crate::FeatureSet) -> Self {
37        Self {
38            target,
39            features,
40            compression_level: None,
41            dictionary_size: None,
42        }
43    }
44
45    pub const fn with_compression_level(mut self, level: u8) -> Self {
46        self.compression_level = Some(level);
47        self
48    }
49
50    pub const fn with_dictionary_size(mut self, size: u64) -> Self {
51        self.dictionary_size = Some(size);
52        self
53    }
54}
55
56impl Default for WriterOptions {
57    fn default() -> Self {
58        Self {
59            target: crate::ArchiveVersion::Rar50,
60            features: crate::FeatureSet::store_only(),
61            compression_level: None,
62            dictionary_size: None,
63        }
64    }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct StoredEntry<'a> {
69    pub name: &'a [u8],
70    pub data: &'a [u8],
71    pub mtime: Option<u32>,
72    pub attributes: u64,
73    pub host_os: u64,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct CompressedEntry<'a> {
78    pub name: &'a [u8],
79    pub data: &'a [u8],
80    pub mtime: Option<u32>,
81    pub attributes: u64,
82    pub host_os: u64,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct StoredServiceEntry<'a> {
87    pub name: &'a [u8],
88    pub data: &'a [u8],
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct StoredEntryWithServices<'a> {
93    pub entry: StoredEntry<'a>,
94    pub services: &'a [StoredServiceEntry<'a>],
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct EncryptedStoredServiceEntry<'a> {
99    pub name: &'a [u8],
100    pub data: &'a [u8],
101    pub password: &'a [u8],
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct EncryptedStoredEntryWithServices<'a> {
106    pub entry: EncryptedStoredEntry<'a>,
107    pub services: &'a [EncryptedStoredServiceEntry<'a>],
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct ArchiveMetadataEntry<'a> {
112    pub name: Option<&'a [u8]>,
113    pub creation_time: Option<u64>,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct EncryptedStoredEntry<'a> {
118    pub name: &'a [u8],
119    pub data: &'a [u8],
120    pub mtime: Option<u32>,
121    pub attributes: u64,
122    pub host_os: u64,
123    pub password: &'a [u8],
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct EncryptedCompressedEntry<'a> {
128    pub name: &'a [u8],
129    pub data: &'a [u8],
130    pub mtime: Option<u32>,
131    pub attributes: u64,
132    pub host_os: u64,
133    pub password: &'a [u8],
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct EncryptedArchiveCommentEntry<'a> {
138    pub data: &'a [u8],
139    pub password: &'a [u8],
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum FilterPolicy {
145    None,
146    AutoSize,
147    Explicit(FilterKind),
148}
149
150#[derive(Debug, Clone)]
151#[non_exhaustive]
152pub struct Rar50Writer<'a> {
153    options: WriterOptions,
154    members: Vec<Rar50WriteMember<'a>>,
155    archive_comment: Option<ArchiveComment<'a>>,
156    archive_metadata: Option<ArchiveMetadataEntry<'a>>,
157    filter_policy: FilterPolicy,
158    recovery_percent: Option<u64>,
159    recovery_password: Option<&'a [u8]>,
160}
161
162#[derive(Debug, Clone)]
163#[non_exhaustive]
164pub struct Rar50VolumeWriter<'a> {
165    options: WriterOptions,
166    entries: Option<Rar50VolumeEntries<'a>>,
167    max_payload_per_volume: Option<usize>,
168    recovery_percent: Option<u64>,
169}
170
171#[derive(Debug, Clone)]
172enum Rar50VolumeEntries<'a> {
173    Stored(StoredEntry<'a>),
174    Compressed(&'a [CompressedEntry<'a>]),
175    EncryptedStored(EncryptedStoredEntry<'a>),
176    EncryptedCompressed(&'a [EncryptedCompressedEntry<'a>]),
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180enum ArchiveComment<'a> {
181    Plain(&'a [u8]),
182    Encrypted(EncryptedArchiveCommentEntry<'a>),
183}
184
185impl<'a> ArchiveComment<'a> {
186    fn encrypted(self) -> Option<EncryptedArchiveCommentEntry<'a>> {
187        match self {
188            Self::Plain(_) => None,
189            Self::Encrypted(comment) => Some(comment),
190        }
191    }
192
193    fn password(self) -> Option<&'a [u8]> {
194        self.encrypted().map(|comment| comment.password)
195    }
196
197    fn plain_only(self) -> Option<Self> {
198        matches!(self, Self::Plain(_)).then_some(self)
199    }
200
201    fn encrypted_only(self) -> Option<Self> {
202        matches!(self, Self::Encrypted(_)).then_some(self)
203    }
204}
205
206impl<'a> Rar50VolumeWriter<'a> {
207    pub fn new(options: WriterOptions) -> Self {
208        Self {
209            options,
210            entries: None,
211            max_payload_per_volume: None,
212            recovery_percent: None,
213        }
214    }
215
216    pub fn stored_entry(mut self, entry: StoredEntry<'a>) -> Self {
217        self.entries = Some(Rar50VolumeEntries::Stored(entry));
218        self
219    }
220
221    pub fn compressed_entries(mut self, entries: &'a [CompressedEntry<'a>]) -> Self {
222        self.entries = Some(Rar50VolumeEntries::Compressed(entries));
223        self
224    }
225
226    pub fn encrypted_stored_entry(mut self, entry: EncryptedStoredEntry<'a>) -> Self {
227        self.entries = Some(Rar50VolumeEntries::EncryptedStored(entry));
228        self
229    }
230
231    pub fn encrypted_compressed_entries(
232        mut self,
233        entries: &'a [EncryptedCompressedEntry<'a>],
234    ) -> Self {
235        self.entries = Some(Rar50VolumeEntries::EncryptedCompressed(entries));
236        self
237    }
238
239    pub fn max_payload_per_volume(mut self, size: usize) -> Self {
240        self.max_payload_per_volume = Some(size);
241        self
242    }
243
244    pub fn recovery_percent(mut self, percent: Option<u64>) -> Self {
245        self.recovery_percent = percent;
246        self
247    }
248
249    pub fn finish(self) -> Result<Vec<Vec<u8>>> {
250        let max_payload_per_volume = self.max_payload_per_volume.ok_or(Error::InvalidHeader(
251            "RAR 5 volume payload size is required",
252        ))?;
253        match self.entries.ok_or(Error::InvalidHeader(
254            "RAR 5 volume writer needs an entry set",
255        ))? {
256            Rar50VolumeEntries::Stored(entry) => write_stored_volumes_impl(
257                entry,
258                self.options,
259                max_payload_per_volume,
260                self.recovery_percent,
261            ),
262            Rar50VolumeEntries::Compressed(entries) => write_compressed_volume_set_impl(
263                entries,
264                self.options,
265                max_payload_per_volume,
266                self.recovery_percent,
267            ),
268            Rar50VolumeEntries::EncryptedStored(entry) => write_encrypted_stored_volumes_impl(
269                entry,
270                self.options,
271                max_payload_per_volume,
272                self.recovery_percent,
273            ),
274            Rar50VolumeEntries::EncryptedCompressed(entries) => {
275                write_encrypted_compressed_volume_set_impl(
276                    entries,
277                    self.options,
278                    max_payload_per_volume,
279                    self.recovery_percent,
280                )
281            }
282        }
283    }
284}
285
286impl<'a> Rar50Writer<'a> {
287    pub fn new(options: WriterOptions) -> Self {
288        Self {
289            options,
290            members: Vec::new(),
291            archive_comment: None,
292            archive_metadata: None,
293            filter_policy: FilterPolicy::None,
294            recovery_percent: None,
295            recovery_password: None,
296        }
297    }
298
299    pub fn stored_entries(mut self, entries: &[StoredEntry<'a>]) -> Self {
300        self.members
301            .extend(entries.iter().copied().map(Rar50WriteMember::Stored));
302        self
303    }
304
305    pub fn compressed_entries(mut self, entries: &[CompressedEntry<'a>]) -> Self {
306        self.members
307            .extend(entries.iter().copied().map(Rar50WriteMember::Compressed));
308        self
309    }
310
311    pub fn encrypted_stored_entries(mut self, entries: &[EncryptedStoredEntry<'a>]) -> Self {
312        self.members.extend(
313            entries
314                .iter()
315                .copied()
316                .map(Rar50WriteMember::EncryptedStored),
317        );
318        self
319    }
320
321    pub fn stored_entries_with_services(mut self, entries: &[StoredEntryWithServices<'a>]) -> Self {
322        self.members.extend(
323            entries
324                .iter()
325                .copied()
326                .map(Rar50WriteMember::StoredWithServices),
327        );
328        self
329    }
330
331    pub fn encrypted_compressed_entries(
332        mut self,
333        entries: &[EncryptedCompressedEntry<'a>],
334    ) -> Self {
335        self.members.extend(
336            entries
337                .iter()
338                .copied()
339                .map(Rar50WriteMember::EncryptedCompressed),
340        );
341        self
342    }
343
344    pub fn encrypted_stored_entries_with_services(
345        mut self,
346        entries: &[EncryptedStoredEntryWithServices<'a>],
347    ) -> Self {
348        self.members.extend(
349            entries
350                .iter()
351                .copied()
352                .map(Rar50WriteMember::EncryptedStoredWithServices),
353        );
354        self
355    }
356
357    pub fn archive_comment(mut self, comment: Option<&'a [u8]>) -> Self {
358        self.archive_comment = comment.map(ArchiveComment::Plain);
359        self
360    }
361
362    pub fn encrypted_archive_comment(
363        mut self,
364        comment: Option<EncryptedArchiveCommentEntry<'a>>,
365    ) -> Self {
366        self.archive_comment = comment.map(ArchiveComment::Encrypted);
367        self
368    }
369
370    pub fn archive_metadata(mut self, metadata: Option<ArchiveMetadataEntry<'a>>) -> Self {
371        self.archive_metadata = metadata;
372        self
373    }
374
375    pub fn filter_policy(mut self, policy: FilterPolicy) -> Self {
376        self.filter_policy = policy;
377        self
378    }
379
380    pub fn recovery_percent(mut self, percent: Option<u64>) -> Self {
381        self.recovery_percent = percent;
382        self
383    }
384
385    pub fn recovery_password(mut self, password: Option<&'a [u8]>) -> Self {
386        self.recovery_password = password;
387        self
388    }
389
390    pub fn finish(self) -> Result<Vec<u8>> {
391        emit_resolved_writer_plan(self.resolve()?)
392    }
393
394    fn resolve(self) -> Result<ResolvedRar50WritePlan<'a>> {
395        let member_kind = self.members.iter().try_fold(None, |seen, member| {
396            let kind = member.kind();
397            if seen.is_some_and(|seen| seen != kind) {
398                return Err(Error::UnsupportedFeature {
399                    version: self.options.target,
400                    feature: "RAR 5 mixed stored/compressed writer plan",
401                });
402            }
403            Ok(Some(kind))
404        })?;
405        if self.options.features.quick_open {
406            validate_options(self.options)?;
407        }
408        if let Some(recovery_percent) = self.recovery_percent {
409            validate_recovery_percent(recovery_percent)?;
410            if self.options.features.quick_open
411                || self.options.features.archive_comment
412                || self.archive_comment.is_some()
413            {
414                return Err(Error::UnsupportedFeature {
415                    version: self.options.target,
416                    feature: "RAR 5 recovery writer option combination",
417                });
418            }
419        }
420        let algorithm_version = rar50_algorithm_version(self.options)?;
421        let compression_method = compression_method_for_level(self.options.compression_level)?;
422        let dictionary_size = dictionary_size_for_options(self.options)?;
423        let encode_options =
424            encode_options_for_level(self.options.compression_level, dictionary_size)?;
425        let encode_option_candidates =
426            encode_option_candidates_for_level(self.options.compression_level, dictionary_size)?;
427
428        let mut resolved_members = Vec::with_capacity(self.members.len());
429        match member_kind.unwrap_or(Rar50WriteMemberKind::Stored) {
430            Rar50WriteMemberKind::Stored => {
431                if self.filter_policy != FilterPolicy::None {
432                    return Err(Error::UnsupportedFeature {
433                        version: self.options.target,
434                        feature: "RAR 5 stored writer filter policy",
435                    });
436                }
437                if self.recovery_percent.is_some() {
438                    validate_recovery_options(self.options)?;
439                } else {
440                    validate_options(self.options)?;
441                }
442                for member in self.members {
443                    let entry = member.into_stored(self.options.target)?;
444                    validate_entry(&entry)?;
445                    resolved_members.push(ResolvedRar50WriteMember::Stored(entry));
446                }
447                Ok(ResolvedRar50WritePlan {
448                    main_flags: 0,
449                    archive_comment: self.archive_comment.and_then(ArchiveComment::plain_only),
450                    archive_metadata: self.archive_metadata,
451                    quick_open: self.options.features.quick_open,
452                    recovery_percent: self.recovery_percent,
453                    header_keys: None,
454                    members: resolved_members,
455                })
456            }
457            Rar50WriteMemberKind::StoredWithServices => {
458                if self.archive_comment.is_some()
459                    || self.archive_metadata.is_some()
460                    || self.recovery_percent.is_some()
461                    || self.filter_policy != FilterPolicy::None
462                {
463                    return Err(Error::UnsupportedFeature {
464                        version: self.options.target,
465                        feature: "RAR 5 stored file-service writer option combination",
466                    });
467                }
468                validate_file_service_options(self.options)?;
469                for member in self.members {
470                    let entry = member.into_stored_with_services(self.options.target)?;
471                    validate_entry(&entry.entry)?;
472                    for service in entry.services {
473                        validate_file_service(service)?;
474                    }
475                    resolved_members.push(ResolvedRar50WriteMember::StoredWithServices(entry));
476                }
477                Ok(ResolvedRar50WritePlan {
478                    main_flags: 0,
479                    archive_comment: None,
480                    archive_metadata: None,
481                    quick_open: false,
482                    recovery_percent: None,
483                    header_keys: None,
484                    members: resolved_members,
485                })
486            }
487            Rar50WriteMemberKind::Compressed => {
488                if self.options.features.quick_open {
489                    return Err(Error::UnsupportedFeature {
490                        version: self.options.target,
491                        feature: "RAR 5 compressed quick-open writer",
492                    });
493                }
494                if self.recovery_percent.is_some() {
495                    validate_compressed_recovery_options(self.options)?;
496                } else {
497                    validate_compressed_options(self.options)?;
498                }
499                if self.filter_policy != FilterPolicy::None && self.options.features.solid {
500                    return Err(Error::UnsupportedFeature {
501                        version: self.options.target,
502                        feature: "RAR 5 solid filtered compressed writer",
503                    });
504                }
505                if self.filter_policy != FilterPolicy::None
506                    && (self.archive_comment.is_some() || self.archive_metadata.is_some())
507                {
508                    return Err(Error::UnsupportedFeature {
509                        version: self.options.target,
510                        feature: "RAR 5 filtered compressed writer service or metadata",
511                    });
512                }
513                let mut solid_encoder = self
514                    .options
515                    .features
516                    .solid
517                    .then(|| Unpack50Encoder::with_options(encode_options));
518                for (index, member) in self.members.into_iter().enumerate() {
519                    let entry = member.into_compressed(self.options.target)?;
520                    validate_compressed_entry(&entry)?;
521                    if compression_method == 0 {
522                        resolved_members.push(ResolvedRar50WriteMember::StoredCompressed(entry));
523                        continue;
524                    }
525                    let (packed, solid_continuation) = if let Some(encoder) = solid_encoder.as_mut()
526                    {
527                        encode_with_solid_reset_policy(
528                            encoder,
529                            entry.data,
530                            algorithm_version,
531                            encode_options,
532                            index,
533                        )?
534                    } else {
535                        (
536                            encode_member_with_filter_policy_candidates(
537                                entry.data,
538                                algorithm_version,
539                                self.filter_policy,
540                                &encode_option_candidates,
541                            )?,
542                            false,
543                        )
544                    };
545                    if should_store_compressed_payload(
546                        entry.data,
547                        &packed,
548                        self.options.features.solid,
549                        self.filter_policy,
550                    ) {
551                        resolved_members.push(ResolvedRar50WriteMember::StoredCompressed(entry));
552                    } else {
553                        resolved_members.push(ResolvedRar50WriteMember::Compressed {
554                            entry,
555                            packed,
556                            algorithm_version,
557                            compression_method,
558                            dictionary_size,
559                            solid_continuation,
560                        });
561                    }
562                }
563                Ok(ResolvedRar50WritePlan {
564                    main_flags: if self.options.features.solid {
565                        MHFL_SOLID
566                    } else {
567                        0
568                    },
569                    archive_comment: self.archive_comment.and_then(ArchiveComment::plain_only),
570                    archive_metadata: self.archive_metadata,
571                    quick_open: false,
572                    recovery_percent: self.recovery_percent,
573                    header_keys: None,
574                    members: resolved_members,
575                })
576            }
577            Rar50WriteMemberKind::EncryptedStored => {
578                if self.recovery_percent.is_some() {
579                    validate_encrypted_recovery_options(self.options)?;
580                } else {
581                    validate_encrypted_options(self.options)?;
582                }
583                let header_keys = if self.options.features.header_encryption {
584                    let password = header_encryption_password(
585                        self.members
586                            .iter()
587                            .map(|member| member.encrypted_stored_password(self.options.target))
588                            .collect::<Result<Vec<_>>>()?
589                            .into_iter()
590                            .chain(self.archive_comment.and_then(ArchiveComment::password))
591                            .chain(self.recovery_password),
592                    )?;
593                    Some(header_encryption_keys(password)?)
594                } else {
595                    None
596                };
597                for member in self.members {
598                    let entry = member.into_encrypted_stored(self.options.target)?;
599                    validate_encrypted_entry(&entry)?;
600                    let encrypted = encrypted_stored_payload(entry.data, entry.password)?;
601                    resolved_members
602                        .push(ResolvedRar50WriteMember::EncryptedStored { entry, encrypted });
603                }
604                Ok(ResolvedRar50WritePlan {
605                    main_flags: 0,
606                    archive_comment: self
607                        .archive_comment
608                        .and_then(ArchiveComment::encrypted_only),
609                    archive_metadata: self.archive_metadata,
610                    quick_open: false,
611                    recovery_percent: self.recovery_percent,
612                    header_keys,
613                    members: resolved_members,
614                })
615            }
616            Rar50WriteMemberKind::EncryptedStoredWithServices => {
617                if self.archive_comment.is_some()
618                    || self.archive_metadata.is_some()
619                    || self.recovery_percent.is_some()
620                    || self.filter_policy != FilterPolicy::None
621                {
622                    return Err(Error::UnsupportedFeature {
623                        version: self.options.target,
624                        feature: "RAR 5 encrypted stored file-service writer option combination",
625                    });
626                }
627                validate_encrypted_file_service_options(self.options)?;
628                let header_keys = if self.options.features.header_encryption {
629                    let mut passwords = Vec::new();
630                    for member in &self.members {
631                        let entry =
632                            member.encrypted_stored_with_services_ref(self.options.target)?;
633                        passwords.push(entry.entry.password);
634                        passwords.extend(entry.services.iter().map(|service| service.password));
635                    }
636                    let password = header_encryption_password(passwords.into_iter())?;
637                    Some(header_encryption_keys(password)?)
638                } else {
639                    None
640                };
641                for member in self.members {
642                    let entry = member.into_encrypted_stored_with_services(self.options.target)?;
643                    validate_encrypted_entry(&entry.entry)?;
644                    for service in entry.services {
645                        validate_encrypted_file_service(service)?;
646                    }
647                    let encrypted =
648                        encrypted_stored_payload(entry.entry.data, entry.entry.password)?;
649                    resolved_members.push(ResolvedRar50WriteMember::EncryptedStoredWithServices {
650                        entry,
651                        encrypted,
652                    });
653                }
654                Ok(ResolvedRar50WritePlan {
655                    main_flags: 0,
656                    archive_comment: None,
657                    archive_metadata: None,
658                    quick_open: false,
659                    recovery_percent: None,
660                    header_keys,
661                    members: resolved_members,
662                })
663            }
664            Rar50WriteMemberKind::EncryptedCompressed => {
665                if self.recovery_percent.is_some() {
666                    validate_encrypted_compressed_recovery_options(self.options)?;
667                } else {
668                    validate_encrypted_compressed_options(self.options)?;
669                }
670                let header_keys = if self.options.features.header_encryption {
671                    let password = header_encryption_password(
672                        self.members
673                            .iter()
674                            .map(|member| member.encrypted_compressed_password(self.options.target))
675                            .collect::<Result<Vec<_>>>()?
676                            .into_iter()
677                            .chain(self.archive_comment.and_then(ArchiveComment::password)),
678                    )?;
679                    Some(header_encryption_keys(password)?)
680                } else {
681                    None
682                };
683                let mut solid_encoder = self
684                    .options
685                    .features
686                    .solid
687                    .then(|| Unpack50Encoder::with_options(encode_options));
688                for (index, member) in self.members.into_iter().enumerate() {
689                    let entry = member.into_encrypted_compressed(self.options.target)?;
690                    validate_encrypted_compressed_entry(&entry)?;
691                    if compression_method == 0 {
692                        let encrypted = encrypted_stored_payload(entry.data, entry.password)?;
693                        resolved_members.push(
694                            ResolvedRar50WriteMember::EncryptedStoredCompressed {
695                                entry,
696                                encrypted,
697                            },
698                        );
699                        continue;
700                    }
701                    let (packed, solid_continuation) = if let Some(encoder) = solid_encoder.as_mut()
702                    {
703                        encode_with_solid_reset_policy(
704                            encoder,
705                            entry.data,
706                            algorithm_version,
707                            encode_options,
708                            index,
709                        )?
710                    } else {
711                        (
712                            encode_member_with_filter_policy_candidates(
713                                entry.data,
714                                algorithm_version,
715                                FilterPolicy::None,
716                                &encode_option_candidates,
717                            )?,
718                            false,
719                        )
720                    };
721                    if should_store_compressed_payload(
722                        entry.data,
723                        &packed,
724                        self.options.features.solid,
725                        FilterPolicy::None,
726                    ) {
727                        let encrypted = encrypted_stored_payload(entry.data, entry.password)?;
728                        resolved_members.push(
729                            ResolvedRar50WriteMember::EncryptedStoredCompressed {
730                                entry,
731                                encrypted,
732                            },
733                        );
734                    } else {
735                        let encrypted = encrypted_payload(&packed, entry.data, entry.password)?;
736                        resolved_members.push(ResolvedRar50WriteMember::EncryptedCompressed {
737                            entry,
738                            encrypted,
739                            algorithm_version,
740                            compression_method,
741                            dictionary_size,
742                            solid_continuation,
743                        });
744                    }
745                }
746                Ok(ResolvedRar50WritePlan {
747                    main_flags: if self.options.features.solid {
748                        MHFL_SOLID
749                    } else {
750                        0
751                    },
752                    archive_comment: self
753                        .archive_comment
754                        .and_then(ArchiveComment::encrypted_only),
755                    archive_metadata: self.archive_metadata,
756                    quick_open: false,
757                    recovery_percent: self.recovery_percent,
758                    header_keys,
759                    members: resolved_members,
760                })
761            }
762        }
763    }
764}
765
766#[derive(Debug, Clone, Copy, PartialEq, Eq)]
767enum Rar50WriteMemberKind {
768    Stored,
769    StoredWithServices,
770    Compressed,
771    EncryptedStored,
772    EncryptedStoredWithServices,
773    EncryptedCompressed,
774}
775
776#[derive(Debug, Clone, Copy, PartialEq, Eq)]
777enum Rar50WriteMember<'a> {
778    Stored(StoredEntry<'a>),
779    StoredWithServices(StoredEntryWithServices<'a>),
780    Compressed(CompressedEntry<'a>),
781    EncryptedStored(EncryptedStoredEntry<'a>),
782    EncryptedStoredWithServices(EncryptedStoredEntryWithServices<'a>),
783    EncryptedCompressed(EncryptedCompressedEntry<'a>),
784}
785
786impl<'a> Rar50WriteMember<'a> {
787    fn kind(self) -> Rar50WriteMemberKind {
788        match self {
789            Self::Stored(_) => Rar50WriteMemberKind::Stored,
790            Self::StoredWithServices(_) => Rar50WriteMemberKind::StoredWithServices,
791            Self::Compressed(_) => Rar50WriteMemberKind::Compressed,
792            Self::EncryptedStored(_) => Rar50WriteMemberKind::EncryptedStored,
793            Self::EncryptedStoredWithServices(_) => {
794                Rar50WriteMemberKind::EncryptedStoredWithServices
795            }
796            Self::EncryptedCompressed(_) => Rar50WriteMemberKind::EncryptedCompressed,
797        }
798    }
799
800    fn into_stored(self, target: crate::ArchiveVersion) -> Result<StoredEntry<'a>> {
801        match self {
802            Self::Stored(entry) => Ok(entry),
803            _ => Err(mixed_member_plan_error(target)),
804        }
805    }
806
807    fn into_stored_with_services(
808        self,
809        target: crate::ArchiveVersion,
810    ) -> Result<StoredEntryWithServices<'a>> {
811        match self {
812            Self::StoredWithServices(entry) => Ok(entry),
813            _ => Err(mixed_member_plan_error(target)),
814        }
815    }
816
817    fn into_compressed(self, target: crate::ArchiveVersion) -> Result<CompressedEntry<'a>> {
818        match self {
819            Self::Compressed(entry) => Ok(entry),
820            _ => Err(mixed_member_plan_error(target)),
821        }
822    }
823
824    fn into_encrypted_stored(
825        self,
826        target: crate::ArchiveVersion,
827    ) -> Result<EncryptedStoredEntry<'a>> {
828        match self {
829            Self::EncryptedStored(entry) => Ok(entry),
830            _ => Err(mixed_member_plan_error(target)),
831        }
832    }
833
834    fn into_encrypted_stored_with_services(
835        self,
836        target: crate::ArchiveVersion,
837    ) -> Result<EncryptedStoredEntryWithServices<'a>> {
838        match self {
839            Self::EncryptedStoredWithServices(entry) => Ok(entry),
840            _ => Err(mixed_member_plan_error(target)),
841        }
842    }
843
844    fn into_encrypted_compressed(
845        self,
846        target: crate::ArchiveVersion,
847    ) -> Result<EncryptedCompressedEntry<'a>> {
848        match self {
849            Self::EncryptedCompressed(entry) => Ok(entry),
850            _ => Err(mixed_member_plan_error(target)),
851        }
852    }
853
854    fn encrypted_stored_password(&self, target: crate::ArchiveVersion) -> Result<&'a [u8]> {
855        match self {
856            Self::EncryptedStored(entry) => Ok(entry.password),
857            _ => Err(mixed_member_plan_error(target)),
858        }
859    }
860
861    fn encrypted_stored_with_services_ref(
862        &self,
863        target: crate::ArchiveVersion,
864    ) -> Result<&EncryptedStoredEntryWithServices<'a>> {
865        match self {
866            Self::EncryptedStoredWithServices(entry) => Ok(entry),
867            _ => Err(mixed_member_plan_error(target)),
868        }
869    }
870
871    fn encrypted_compressed_password(&self, target: crate::ArchiveVersion) -> Result<&'a [u8]> {
872        match self {
873            Self::EncryptedCompressed(entry) => Ok(entry.password),
874            _ => Err(mixed_member_plan_error(target)),
875        }
876    }
877}
878
879fn mixed_member_plan_error(target: crate::ArchiveVersion) -> Error {
880    Error::UnsupportedFeature {
881        version: target,
882        feature: "RAR 5 mixed stored/compressed writer plan",
883    }
884}
885
886struct ResolvedRar50WritePlan<'a> {
887    main_flags: u64,
888    archive_comment: Option<ArchiveComment<'a>>,
889    archive_metadata: Option<ArchiveMetadataEntry<'a>>,
890    quick_open: bool,
891    recovery_percent: Option<u64>,
892    header_keys: Option<HeaderEncryptionKeys>,
893    members: Vec<ResolvedRar50WriteMember<'a>>,
894}
895
896enum ResolvedRar50WriteMember<'a> {
897    Stored(StoredEntry<'a>),
898    StoredWithServices(StoredEntryWithServices<'a>),
899    StoredCompressed(CompressedEntry<'a>),
900    Compressed {
901        entry: CompressedEntry<'a>,
902        packed: Vec<u8>,
903        algorithm_version: u8,
904        compression_method: u8,
905        dictionary_size: u64,
906        solid_continuation: bool,
907    },
908    EncryptedStored {
909        entry: EncryptedStoredEntry<'a>,
910        encrypted: EncryptedStoredPayload,
911    },
912    EncryptedStoredWithServices {
913        entry: EncryptedStoredEntryWithServices<'a>,
914        encrypted: EncryptedStoredPayload,
915    },
916    EncryptedStoredCompressed {
917        entry: EncryptedCompressedEntry<'a>,
918        encrypted: EncryptedStoredPayload,
919    },
920    EncryptedCompressed {
921        entry: EncryptedCompressedEntry<'a>,
922        encrypted: EncryptedStoredPayload,
923        algorithm_version: u8,
924        compression_method: u8,
925        dictionary_size: u64,
926        solid_continuation: bool,
927    },
928}
929
930fn emit_resolved_writer_plan(plan: ResolvedRar50WritePlan<'_>) -> Result<Vec<u8>> {
931    if plan.quick_open {
932        return resolve_writer_plan_offset(
933            |quick_open_offset| {
934                emit_resolved_writer_plan_pass(&plan, Some(quick_open_offset), None)
935                    .map(|(out, next_quick_open_offset, _)| (out, next_quick_open_offset))
936            },
937            "RAR 5 quick-open pass did not report an offset",
938            "RAR 5 quick-open offset did not converge",
939        );
940    }
941    if plan.recovery_percent.is_some() {
942        return resolve_writer_plan_offset(
943            |recovery_offset| {
944                emit_resolved_writer_plan_pass(&plan, None, Some(recovery_offset))
945                    .map(|(out, _, next_recovery_offset)| (out, next_recovery_offset))
946            },
947            "RAR 5 recovery pass did not report an offset",
948            "RAR 5 recovery offset did not converge",
949        );
950    }
951    emit_resolved_writer_plan_pass(&plan, None, None).map(|(out, _, _)| out)
952}
953
954fn resolve_writer_plan_offset<F>(
955    mut pass: F,
956    missing_offset_error: &'static str,
957    convergence_error: &'static str,
958) -> Result<Vec<u8>>
959where
960    F: FnMut(u64) -> Result<(Vec<u8>, Option<u64>)>,
961{
962    let mut offset = 0;
963    for _ in 0..4 {
964        let (out, next_offset) = pass(offset)?;
965        if next_offset == Some(offset) {
966            return Ok(out);
967        }
968        offset = next_offset.ok_or(Error::InvalidHeader(missing_offset_error))?;
969    }
970    Err(Error::InvalidHeader(convergence_error))
971}
972
973fn emit_resolved_writer_plan_pass(
974    plan: &ResolvedRar50WritePlan<'_>,
975    quick_open_offset: Option<u64>,
976    recovery_offset: Option<u64>,
977) -> Result<(Vec<u8>, Option<u64>, Option<u64>)> {
978    let mut out = Vec::new();
979    let mut cached_headers = Vec::new();
980    out.extend_from_slice(RAR50_SIGNATURE);
981    let main_extra =
982        resolved_main_extra(plan.archive_metadata, quick_open_offset, recovery_offset)?;
983    let main_flags = plan.main_flags
984        | if plan.recovery_percent.is_some() {
985            MHFL_RECOVERY
986        } else {
987            0
988        };
989    if let Some(header_keys) = &plan.header_keys {
990        write_head_crypt(&mut out, header_keys)?;
991        out.extend_from_slice(&encrypted_main_header_block(
992            &header_keys.keys,
993            main_flags,
994            None,
995            &main_extra,
996        )?);
997    } else {
998        write_main_header(&mut out, main_flags, None, &main_extra)?;
999    }
1000    if let Some(comment) = plan.archive_comment {
1001        match comment {
1002            ArchiveComment::Plain(comment) => {
1003                if plan.quick_open {
1004                    write_stored_service_with_cache(
1005                        &mut out,
1006                        &mut cached_headers,
1007                        b"CMT",
1008                        comment,
1009                    )?;
1010                } else {
1011                    write_stored_service(&mut out, b"CMT", comment)?;
1012                }
1013            }
1014            ArchiveComment::Encrypted(comment) => {
1015                write_encrypted_stored_service_with_header_keys(
1016                    &mut out,
1017                    b"CMT",
1018                    comment,
1019                    plan.header_keys.as_ref().map(|keys| &keys.keys),
1020                )?;
1021            }
1022        }
1023    }
1024    for member in &plan.members {
1025        match member {
1026            ResolvedRar50WriteMember::Stored(entry) => {
1027                if plan.quick_open {
1028                    write_stored_entry_with_cache(&mut out, &mut cached_headers, entry)?;
1029                } else {
1030                    write_stored_entry(&mut out, entry)?;
1031                }
1032            }
1033            ResolvedRar50WriteMember::StoredWithServices(entry) => {
1034                write_stored_entry(&mut out, &entry.entry)?;
1035                for service in entry.services {
1036                    write_stored_service(&mut out, service.name, service.data)?;
1037                }
1038            }
1039            ResolvedRar50WriteMember::StoredCompressed(entry) => {
1040                write_stored_compressed_entry(&mut out, entry)?;
1041            }
1042            ResolvedRar50WriteMember::Compressed {
1043                entry,
1044                packed,
1045                algorithm_version,
1046                compression_method,
1047                dictionary_size,
1048                solid_continuation,
1049            } => write_compressed_entry_payload(
1050                &mut out,
1051                entry,
1052                packed,
1053                *algorithm_version,
1054                *compression_method,
1055                *dictionary_size,
1056                *solid_continuation,
1057            )?,
1058            ResolvedRar50WriteMember::EncryptedStored { entry, encrypted } => {
1059                write_encrypted_stored_entry_fragment_with_header_keys(
1060                    &mut out,
1061                    entry,
1062                    &encrypted.data,
1063                    encrypted,
1064                    false,
1065                    false,
1066                    plan.header_keys.as_ref().map(|keys| &keys.keys),
1067                )?
1068            }
1069            ResolvedRar50WriteMember::EncryptedStoredWithServices { entry, encrypted } => {
1070                write_encrypted_stored_entry_fragment_with_header_keys(
1071                    &mut out,
1072                    &entry.entry,
1073                    &encrypted.data,
1074                    encrypted,
1075                    false,
1076                    false,
1077                    plan.header_keys.as_ref().map(|keys| &keys.keys),
1078                )?;
1079                for service in entry.services {
1080                    write_encrypted_stored_service_with_header_keys(
1081                        &mut out,
1082                        service.name,
1083                        EncryptedArchiveCommentEntry {
1084                            data: service.data,
1085                            password: service.password,
1086                        },
1087                        plan.header_keys.as_ref().map(|keys| &keys.keys),
1088                    )?;
1089                }
1090            }
1091            ResolvedRar50WriteMember::EncryptedStoredCompressed { entry, encrypted } => {
1092                write_encrypted_stored_compressed_entry_with_header_keys(
1093                    &mut out,
1094                    entry,
1095                    encrypted,
1096                    plan.header_keys.as_ref().map(|keys| &keys.keys),
1097                )?;
1098            }
1099            ResolvedRar50WriteMember::EncryptedCompressed {
1100                entry,
1101                encrypted,
1102                algorithm_version,
1103                compression_method,
1104                dictionary_size,
1105                solid_continuation,
1106            } => write_encrypted_compressed_entry_fragment_with_header_keys(
1107                &mut out,
1108                EncryptedCompressedFragment {
1109                    entry,
1110                    data: &encrypted.data,
1111                    encrypted,
1112                    algorithm_version: *algorithm_version,
1113                    compression_method: *compression_method,
1114                    dictionary_size: *dictionary_size,
1115                    solid_continuation: *solid_continuation,
1116                    split_before: false,
1117                    split_after: false,
1118                },
1119                plan.header_keys.as_ref().map(|keys| &keys.keys),
1120            )?,
1121        }
1122    }
1123    let next_quick_open_offset = if plan.quick_open {
1124        let qo_pos = out.len();
1125        let qo_payload = quick_open_payload(&cached_headers, qo_pos)?;
1126        write_stored_service(&mut out, b"QO", &qo_payload)?;
1127        Some((qo_pos - RAR50_SIGNATURE.len()) as u64)
1128    } else {
1129        None
1130    };
1131    let next_recovery_offset = if let Some(recovery_percent) = plan.recovery_percent {
1132        let rr_pos = out.len();
1133        if let Some(header_keys) = &plan.header_keys {
1134            write_header_encrypted_recovery_service(&mut out, recovery_percent, &header_keys.keys)?;
1135        } else {
1136            write_recovery_service(&mut out, recovery_percent)?;
1137        }
1138        Some((rr_pos - RAR50_SIGNATURE.len()) as u64)
1139    } else {
1140        None
1141    };
1142    if let Some(header_keys) = &plan.header_keys {
1143        out.extend_from_slice(&encrypted_header_block(
1144            &header_keys.keys,
1145            HEAD_END,
1146            0,
1147            None,
1148            &[],
1149            &[],
1150            &[],
1151        )?);
1152    } else {
1153        write_block(&mut out, HEAD_END, 0, None, &[], &[], &[])?;
1154    }
1155    Ok((out, next_quick_open_offset, next_recovery_offset))
1156}
1157
1158fn resolved_main_extra(
1159    archive_metadata: Option<ArchiveMetadataEntry<'_>>,
1160    quick_open_offset: Option<u64>,
1161    recovery_offset: Option<u64>,
1162) -> Result<Vec<u8>> {
1163    let mut main_extra = Vec::new();
1164    let locator_quick_open_offset = quick_open_offset.or_else(|| archive_metadata.map(|_| 0));
1165    if locator_quick_open_offset.is_some() || recovery_offset.is_some() {
1166        write_locator_record(&mut main_extra, locator_quick_open_offset, recovery_offset);
1167    }
1168    if let Some(archive_metadata) = archive_metadata {
1169        main_extra.extend_from_slice(&archive_metadata_record(archive_metadata)?);
1170    }
1171    Ok(main_extra)
1172}
1173
1174fn write_main_header(
1175    out: &mut Vec<u8>,
1176    archive_flags: u64,
1177    volume_number: Option<u64>,
1178    extra: &[u8],
1179) -> Result<()> {
1180    let mut specific = Vec::new();
1181    write_vint(&mut specific, archive_flags);
1182    if let Some(volume_number) = volume_number {
1183        write_vint(&mut specific, volume_number);
1184    }
1185    write_block(
1186        out,
1187        HEAD_MAIN,
1188        if extra.is_empty() { 0 } else { HFL_EXTRA },
1189        None,
1190        &specific,
1191        extra,
1192        &[],
1193    )
1194}
1195
1196fn encrypted_main_header_block(
1197    keys: &Rar50Keys,
1198    archive_flags: u64,
1199    volume_number: Option<u64>,
1200    extra: &[u8],
1201) -> Result<Vec<u8>> {
1202    let mut specific = Vec::new();
1203    write_vint(&mut specific, archive_flags);
1204    if let Some(volume_number) = volume_number {
1205        write_vint(&mut specific, volume_number);
1206    }
1207    encrypted_header_block(
1208        keys,
1209        HEAD_MAIN,
1210        if extra.is_empty() { 0 } else { HFL_EXTRA },
1211        None,
1212        &specific,
1213        extra,
1214        &[],
1215    )
1216}
1217
1218fn validate_options(options: WriterOptions) -> Result<()> {
1219    validate_plain_options(options, false)
1220}
1221
1222fn validate_recovery_options(options: WriterOptions) -> Result<()> {
1223    validate_plain_options(options, true)
1224}
1225
1226fn validate_plain_options(options: WriterOptions, allow_recovery_record: bool) -> Result<()> {
1227    validate_compression_level(options)?;
1228    if !matches!(
1229        options.target,
1230        crate::ArchiveVersion::Rar50 | crate::ArchiveVersion::Rar70
1231    ) {
1232        return Err(Error::UnsupportedVersion(options.target));
1233    }
1234    let mut allowed = crate::FeatureSet::store_only();
1235    allowed.archive_comment = options.features.archive_comment;
1236    allowed.quick_open = options.features.quick_open;
1237    if allow_recovery_record {
1238        allowed.recovery_record = options.features.recovery_record;
1239    }
1240    if options.features != allowed {
1241        return Err(Error::UnsupportedFeature {
1242            version: options.target,
1243            feature: "RAR 5 writer feature",
1244        });
1245    }
1246    Ok(())
1247}
1248
1249fn validate_file_service_options(options: WriterOptions) -> Result<()> {
1250    validate_compression_level(options)?;
1251    if !matches!(
1252        options.target,
1253        crate::ArchiveVersion::Rar50 | crate::ArchiveVersion::Rar70
1254    ) {
1255        return Err(Error::UnsupportedVersion(options.target));
1256    }
1257    let mut allowed = crate::FeatureSet::store_only();
1258    allowed.file_comment = options.features.file_comment;
1259    if options.features != allowed {
1260        return Err(Error::UnsupportedFeature {
1261            version: options.target,
1262            feature: "RAR 5 stored file-service writer feature",
1263        });
1264    }
1265    Ok(())
1266}
1267
1268fn validate_compressed_options(options: WriterOptions) -> Result<()> {
1269    validate_compressed_feature_options(options, false)
1270}
1271
1272fn validate_compressed_recovery_options(options: WriterOptions) -> Result<()> {
1273    validate_compressed_feature_options(options, true)
1274}
1275
1276fn validate_compressed_feature_options(
1277    options: WriterOptions,
1278    allow_recovery_record: bool,
1279) -> Result<()> {
1280    validate_compression_level(options)?;
1281    if !matches!(
1282        options.target,
1283        crate::ArchiveVersion::Rar50 | crate::ArchiveVersion::Rar70
1284    ) {
1285        return Err(Error::UnsupportedVersion(options.target));
1286    }
1287    let mut allowed = crate::FeatureSet::store_only();
1288    allowed.solid = options.features.solid;
1289    allowed.archive_comment = options.features.archive_comment;
1290    if allow_recovery_record {
1291        allowed.recovery_record = options.features.recovery_record;
1292    }
1293    if options.features != allowed {
1294        return Err(Error::UnsupportedFeature {
1295            version: options.target,
1296            feature: "RAR 5 compressed writer feature",
1297        });
1298    }
1299    Ok(())
1300}
1301
1302fn validate_encrypted_compressed_options(options: WriterOptions) -> Result<()> {
1303    validate_encrypted_compressed_feature_options(options, false)
1304}
1305
1306fn validate_encrypted_compressed_recovery_options(options: WriterOptions) -> Result<()> {
1307    validate_encrypted_compressed_feature_options(options, true)
1308}
1309
1310fn validate_encrypted_compressed_feature_options(
1311    options: WriterOptions,
1312    allow_recovery_record: bool,
1313) -> Result<()> {
1314    validate_compression_level(options)?;
1315    if !matches!(
1316        options.target,
1317        crate::ArchiveVersion::Rar50 | crate::ArchiveVersion::Rar70
1318    ) {
1319        return Err(Error::UnsupportedVersion(options.target));
1320    }
1321    let mut allowed = crate::FeatureSet::store_only();
1322    allowed.file_encryption = true;
1323    allowed.header_encryption = options.features.header_encryption;
1324    allowed.solid = options.features.solid;
1325    allowed.archive_comment = options.features.archive_comment;
1326    if allow_recovery_record {
1327        allowed.recovery_record = options.features.recovery_record;
1328    }
1329    if options.features != allowed {
1330        return Err(Error::UnsupportedFeature {
1331            version: options.target,
1332            feature: "RAR 5 encrypted compressed writer feature",
1333        });
1334    }
1335    Ok(())
1336}
1337
1338struct CachedHeader {
1339    offset: usize,
1340    header: Vec<u8>,
1341}
1342
1343struct BlockParts<'a> {
1344    header_type: u64,
1345    flags: u64,
1346    data_size: Option<u64>,
1347    type_specific: &'a [u8],
1348    extra: &'a [u8],
1349    data: &'a [u8],
1350}
1351
1352fn validate_encrypted_options(options: WriterOptions) -> Result<()> {
1353    validate_encrypted_feature_options(options, false)
1354}
1355
1356fn validate_encrypted_recovery_options(options: WriterOptions) -> Result<()> {
1357    validate_encrypted_feature_options(options, true)
1358}
1359
1360fn validate_encrypted_feature_options(
1361    options: WriterOptions,
1362    allow_recovery_record: bool,
1363) -> Result<()> {
1364    validate_compression_level(options)?;
1365    if !matches!(
1366        options.target,
1367        crate::ArchiveVersion::Rar50 | crate::ArchiveVersion::Rar70
1368    ) {
1369        return Err(Error::UnsupportedVersion(options.target));
1370    }
1371    let mut allowed = crate::FeatureSet::store_only();
1372    allowed.file_encryption = true;
1373    allowed.header_encryption = options.features.header_encryption;
1374    allowed.archive_comment = options.features.archive_comment;
1375    if allow_recovery_record {
1376        allowed.recovery_record = options.features.recovery_record;
1377    }
1378    if options.features != allowed {
1379        return Err(Error::UnsupportedFeature {
1380            version: options.target,
1381            feature: "RAR 5 encrypted stored writer feature",
1382        });
1383    }
1384    Ok(())
1385}
1386
1387fn validate_encrypted_file_service_options(options: WriterOptions) -> Result<()> {
1388    validate_compression_level(options)?;
1389    if !matches!(
1390        options.target,
1391        crate::ArchiveVersion::Rar50 | crate::ArchiveVersion::Rar70
1392    ) {
1393        return Err(Error::UnsupportedVersion(options.target));
1394    }
1395    let mut allowed = crate::FeatureSet::store_only();
1396    allowed.file_encryption = true;
1397    allowed.header_encryption = options.features.header_encryption;
1398    allowed.file_comment = options.features.file_comment;
1399    if options.features != allowed {
1400        return Err(Error::UnsupportedFeature {
1401            version: options.target,
1402            feature: "RAR 5 encrypted stored file-service writer feature",
1403        });
1404    }
1405    Ok(())
1406}
1407
1408struct HeaderEncryptionKeys {
1409    keys: Rar50Keys,
1410    salt: [u8; 16],
1411}
1412
1413fn header_encryption_keys(password: &[u8]) -> Result<HeaderEncryptionKeys> {
1414    let mut salt = [0u8; 16];
1415    getrandom::fill(&mut salt)
1416        .map_err(|_| Error::InvalidHeader("RAR 5 writer could not generate encryption salt"))?;
1417    let keys = Rar50Keys::derive(password, salt, 0).map_err(super::map_rar50_crypto_error)?;
1418    Ok(HeaderEncryptionKeys { keys, salt })
1419}
1420
1421fn header_encryption_password<'a>(
1422    mut passwords: impl Iterator<Item = &'a [u8]>,
1423) -> Result<&'a [u8]> {
1424    let first = passwords.next().ok_or(Error::NeedPassword)?;
1425    for password in passwords {
1426        if password != first {
1427            return Err(Error::InvalidHeader(
1428                "RAR 5 header-encrypted writer needs one shared password",
1429            ));
1430        }
1431    }
1432    Ok(first)
1433}
1434
1435fn write_head_crypt(out: &mut Vec<u8>, header_keys: &HeaderEncryptionKeys) -> Result<()> {
1436    let mut specific = Vec::new();
1437    write_vint(&mut specific, 0);
1438    write_vint(&mut specific, 0x0001);
1439    specific.push(0);
1440    specific.extend_from_slice(&header_keys.salt);
1441    specific.extend_from_slice(&header_keys.keys.password_check_record());
1442    write_block(out, HEAD_CRYPT, 0, None, &specific, &[], &[])
1443}
1444
1445fn archive_metadata_record(metadata: ArchiveMetadataEntry<'_>) -> Result<Vec<u8>> {
1446    if metadata.name.is_none() && metadata.creation_time.is_none() {
1447        return Err(Error::InvalidHeader(
1448            "RAR 5 archive metadata writer needs a name or creation time",
1449        ));
1450    }
1451    if metadata.name.is_some() && metadata.creation_time.is_none() {
1452        return Err(Error::InvalidHeader(
1453            "RAR 5 archive metadata name needs a creation time",
1454        ));
1455    }
1456    let mut flags = 0;
1457    if metadata.name.is_some() {
1458        flags |= MHEXTRA_ARCHIVE_METADATA_NAME;
1459    }
1460    if metadata.creation_time.is_some() {
1461        flags |= MHEXTRA_ARCHIVE_METADATA_TIME;
1462    }
1463
1464    let mut record = Vec::new();
1465    write_vint(&mut record, flags);
1466    if let Some(name) = metadata.name {
1467        if name.is_empty() {
1468            return Err(Error::InvalidHeader("RAR 5 archive metadata name is empty"));
1469        }
1470        write_vint(&mut record, name.len() as u64);
1471        record.extend_from_slice(name);
1472    }
1473    if let Some(creation_time) = metadata.creation_time {
1474        record.extend_from_slice(&creation_time.to_le_bytes());
1475    }
1476
1477    let mut extra = Vec::new();
1478    write_extra_record(&mut extra, MHEXTRA_ARCHIVE_METADATA, &record);
1479    Ok(extra)
1480}
1481
1482fn write_locator_record(
1483    out: &mut Vec<u8>,
1484    quick_open_offset: Option<u64>,
1485    recovery_record_offset: Option<u64>,
1486) {
1487    let mut flags = 0;
1488    if quick_open_offset.is_some() {
1489        flags |= MHEXTRA_LOCATOR_QUICK_OPEN;
1490    }
1491    if recovery_record_offset.is_some() {
1492        flags |= MHEXTRA_LOCATOR_RECOVERY;
1493    }
1494
1495    let mut record = Vec::new();
1496    write_vint(&mut record, flags);
1497    if let Some(quick_open_offset) = quick_open_offset {
1498        write_vint(&mut record, quick_open_offset);
1499    }
1500    if let Some(recovery_record_offset) = recovery_record_offset {
1501        write_vint(&mut record, recovery_record_offset);
1502    }
1503    write_extra_record(out, MHEXTRA_LOCATOR, &record);
1504}
1505
1506fn write_stored_entry(out: &mut Vec<u8>, entry: &StoredEntry<'_>) -> Result<()> {
1507    validate_entry(entry)?;
1508    write_stored_entry_fragment(
1509        out,
1510        entry,
1511        entry.data,
1512        entry.data.len() as u64,
1513        Some(crc32(entry.data)),
1514        false,
1515        false,
1516    )
1517}
1518
1519fn write_stored_compressed_entry(out: &mut Vec<u8>, entry: &CompressedEntry<'_>) -> Result<()> {
1520    validate_compressed_entry(entry)?;
1521    let stored = stored_entry_from_compressed_entry(entry);
1522    write_stored_entry(out, &stored)
1523}
1524
1525fn stored_entry_from_compressed_entry<'a>(entry: &CompressedEntry<'a>) -> StoredEntry<'a> {
1526    StoredEntry {
1527        name: entry.name,
1528        data: entry.data,
1529        mtime: entry.mtime,
1530        attributes: entry.attributes,
1531        host_os: entry.host_os,
1532    }
1533}
1534
1535fn write_compressed_entry_payload(
1536    out: &mut Vec<u8>,
1537    entry: &CompressedEntry<'_>,
1538    packed: &[u8],
1539    algorithm_version: u8,
1540    compression_method: u8,
1541    dictionary_size: u64,
1542    solid_continuation: bool,
1543) -> Result<()> {
1544    let mut extra = Vec::new();
1545    write_hash_record(&mut extra, entry.data);
1546    let compression_info = compression_info(
1547        algorithm_version,
1548        compression_method,
1549        dictionary_size,
1550        solid_continuation,
1551    )?;
1552    let specific = file_specific(
1553        entry.name,
1554        entry.data.len() as u64,
1555        Some(crc32(entry.data)),
1556        entry.attributes,
1557        entry.mtime,
1558        compression_info,
1559        entry.host_os,
1560    )?;
1561    write_block(
1562        out,
1563        HEAD_FILE,
1564        HFL_EXTRA | HFL_DATA,
1565        Some(packed.len() as u64),
1566        &specific,
1567        &extra,
1568        packed,
1569    )
1570}
1571
1572struct CompressedFragment<'a, 'b> {
1573    entry: &'a CompressedEntry<'b>,
1574    data: &'a [u8],
1575    algorithm_version: u8,
1576    compression_method: u8,
1577    dictionary_size: u64,
1578    solid_continuation: bool,
1579    split_before: bool,
1580    split_after: bool,
1581}
1582
1583fn write_compressed_entry_fragment(
1584    out: &mut Vec<u8>,
1585    fragment: CompressedFragment<'_, '_>,
1586) -> Result<()> {
1587    let CompressedFragment {
1588        entry,
1589        data,
1590        algorithm_version,
1591        compression_method,
1592        dictionary_size,
1593        solid_continuation,
1594        split_before,
1595        split_after,
1596    } = fragment;
1597
1598    let mut extra = Vec::new();
1599    if !split_after {
1600        write_hash_record(&mut extra, entry.data);
1601    }
1602    let compression_info = compression_info(
1603        algorithm_version,
1604        compression_method,
1605        dictionary_size,
1606        solid_continuation,
1607    )?;
1608    let specific = file_specific(
1609        entry.name,
1610        entry.data.len() as u64,
1611        (!split_after).then_some(crc32(entry.data)),
1612        entry.attributes,
1613        entry.mtime,
1614        compression_info,
1615        entry.host_os,
1616    )?;
1617    let mut block_flags = HFL_DATA;
1618    if split_before {
1619        block_flags |= HFL_SPLIT_BEFORE;
1620    }
1621    if split_after {
1622        block_flags |= HFL_SPLIT_AFTER;
1623    }
1624    if !extra.is_empty() {
1625        block_flags |= HFL_EXTRA;
1626    }
1627
1628    write_block(
1629        out,
1630        HEAD_FILE,
1631        block_flags,
1632        Some(data.len() as u64),
1633        &specific,
1634        &extra,
1635        data,
1636    )
1637}
1638
1639fn write_stored_entry_with_cache(
1640    out: &mut Vec<u8>,
1641    cached_headers: &mut Vec<CachedHeader>,
1642    entry: &StoredEntry<'_>,
1643) -> Result<()> {
1644    validate_entry(entry)?;
1645    let mut extra = Vec::new();
1646    write_hash_record(&mut extra, entry.data);
1647    let specific = stored_file_specific(
1648        entry.name,
1649        entry.data.len() as u64,
1650        Some(crc32(entry.data)),
1651        entry.attributes,
1652        entry.mtime,
1653        entry.host_os,
1654    )?;
1655    write_block_with_cache(
1656        out,
1657        cached_headers,
1658        BlockParts {
1659            header_type: HEAD_FILE,
1660            flags: HFL_EXTRA | HFL_DATA,
1661            data_size: Some(entry.data.len() as u64),
1662            type_specific: &specific,
1663            extra: &extra,
1664            data: entry.data,
1665        },
1666    )
1667}
1668
1669struct EncryptedStoredPayload {
1670    data: Vec<u8>,
1671    salt: [u8; 16],
1672    iv: [u8; 16],
1673    check_value: [u8; 12],
1674    crc32_mac: u32,
1675    blake2sp_mac: [u8; 32],
1676}
1677
1678fn encrypted_stored_payload(data: &[u8], password: &[u8]) -> Result<EncryptedStoredPayload> {
1679    encrypted_payload(data, data, password)
1680}
1681
1682fn encrypted_payload(
1683    packed_data: &[u8],
1684    integrity_data: &[u8],
1685    password: &[u8],
1686) -> Result<EncryptedStoredPayload> {
1687    let mut salt = [0u8; 16];
1688    let mut iv = [0u8; 16];
1689    getrandom::fill(&mut salt)
1690        .map_err(|_| Error::InvalidHeader("RAR 5 writer could not generate encryption salt"))?;
1691    getrandom::fill(&mut iv)
1692        .map_err(|_| Error::InvalidHeader("RAR 5 writer could not generate encryption IV"))?;
1693    let keys = Rar50Keys::derive(password, salt, 0).map_err(super::map_rar50_crypto_error)?;
1694
1695    let mut encrypted_data = packed_data.to_vec();
1696    let padded_len = encrypted_data
1697        .len()
1698        .checked_add(15)
1699        .ok_or(Error::InvalidHeader("RAR 5 encrypted data size overflows"))?
1700        & !15;
1701    encrypted_data.resize(padded_len, 0);
1702    Rar50Cipher::new(keys.key, iv)
1703        .encrypt_in_place(&mut encrypted_data)
1704        .map_err(super::map_rar50_crypto_error)?;
1705
1706    Ok(EncryptedStoredPayload {
1707        data: encrypted_data,
1708        salt,
1709        iv,
1710        check_value: keys.password_check_record(),
1711        crc32_mac: keys.mac_crc32(crc32(integrity_data)),
1712        blake2sp_mac: keys.mac_hash32(blake2sp::hash(integrity_data)),
1713    })
1714}
1715
1716fn write_encrypted_stored_entry_fragment_with_header_keys(
1717    out: &mut Vec<u8>,
1718    entry: &EncryptedStoredEntry<'_>,
1719    data: &[u8],
1720    encrypted: &EncryptedStoredPayload,
1721    split_before: bool,
1722    split_after: bool,
1723    header_keys: Option<&Rar50Keys>,
1724) -> Result<()> {
1725    let mut extra = Vec::new();
1726    write_file_encryption_record(
1727        &mut extra,
1728        encrypted.salt,
1729        encrypted.iv,
1730        encrypted.check_value,
1731    );
1732    if !split_after {
1733        write_hash_record_with_value(&mut extra, encrypted.blake2sp_mac);
1734    }
1735
1736    let specific = stored_file_specific(
1737        entry.name,
1738        entry.data.len() as u64,
1739        (!split_after).then_some(encrypted.crc32_mac),
1740        entry.attributes,
1741        entry.mtime,
1742        entry.host_os,
1743    )?;
1744    let mut block_flags = HFL_EXTRA | HFL_DATA;
1745    if split_before {
1746        block_flags |= HFL_SPLIT_BEFORE;
1747    }
1748    if split_after {
1749        block_flags |= HFL_SPLIT_AFTER;
1750    }
1751
1752    if let Some(header_keys) = header_keys {
1753        out.extend_from_slice(&encrypted_header_block(
1754            header_keys,
1755            HEAD_FILE,
1756            block_flags,
1757            Some(data.len() as u64),
1758            &specific,
1759            &extra,
1760            data,
1761        )?);
1762        Ok(())
1763    } else {
1764        write_block(
1765            out,
1766            HEAD_FILE,
1767            block_flags,
1768            Some(data.len() as u64),
1769            &specific,
1770            &extra,
1771            data,
1772        )
1773    }
1774}
1775
1776fn write_encrypted_stored_compressed_entry_with_header_keys(
1777    out: &mut Vec<u8>,
1778    entry: &EncryptedCompressedEntry<'_>,
1779    encrypted: &EncryptedStoredPayload,
1780    header_keys: Option<&Rar50Keys>,
1781) -> Result<()> {
1782    validate_encrypted_compressed_entry(entry)?;
1783    let stored = encrypted_stored_entry_from_compressed_entry(entry);
1784    write_encrypted_stored_entry_fragment_with_header_keys(
1785        out,
1786        &stored,
1787        &encrypted.data,
1788        encrypted,
1789        false,
1790        false,
1791        header_keys,
1792    )
1793}
1794
1795fn encrypted_stored_entry_from_compressed_entry<'a>(
1796    entry: &EncryptedCompressedEntry<'a>,
1797) -> EncryptedStoredEntry<'a> {
1798    EncryptedStoredEntry {
1799        name: entry.name,
1800        data: entry.data,
1801        mtime: entry.mtime,
1802        attributes: entry.attributes,
1803        host_os: entry.host_os,
1804        password: entry.password,
1805    }
1806}
1807
1808struct EncryptedCompressedFragment<'a, 'b> {
1809    entry: &'a EncryptedCompressedEntry<'b>,
1810    data: &'a [u8],
1811    encrypted: &'a EncryptedStoredPayload,
1812    algorithm_version: u8,
1813    compression_method: u8,
1814    dictionary_size: u64,
1815    solid_continuation: bool,
1816    split_before: bool,
1817    split_after: bool,
1818}
1819
1820fn write_encrypted_compressed_entry_fragment_with_header_keys(
1821    out: &mut Vec<u8>,
1822    fragment: EncryptedCompressedFragment<'_, '_>,
1823    header_keys: Option<&Rar50Keys>,
1824) -> Result<()> {
1825    let EncryptedCompressedFragment {
1826        entry,
1827        data,
1828        encrypted,
1829        algorithm_version,
1830        compression_method,
1831        dictionary_size,
1832        solid_continuation,
1833        split_before,
1834        split_after,
1835    } = fragment;
1836
1837    let mut extra = Vec::new();
1838    write_file_encryption_record(
1839        &mut extra,
1840        encrypted.salt,
1841        encrypted.iv,
1842        encrypted.check_value,
1843    );
1844    if !split_after {
1845        write_hash_record_with_value(&mut extra, encrypted.blake2sp_mac);
1846    }
1847
1848    let compression_info = compression_info(
1849        algorithm_version,
1850        compression_method,
1851        dictionary_size,
1852        solid_continuation,
1853    )?;
1854    let specific = file_specific(
1855        entry.name,
1856        entry.data.len() as u64,
1857        (!split_after).then_some(encrypted.crc32_mac),
1858        entry.attributes,
1859        entry.mtime,
1860        compression_info,
1861        entry.host_os,
1862    )?;
1863    let mut block_flags = HFL_EXTRA | HFL_DATA;
1864    if split_before {
1865        block_flags |= HFL_SPLIT_BEFORE;
1866    }
1867    if split_after {
1868        block_flags |= HFL_SPLIT_AFTER;
1869    }
1870
1871    if let Some(header_keys) = header_keys {
1872        out.extend_from_slice(&encrypted_header_block(
1873            header_keys,
1874            HEAD_FILE,
1875            block_flags,
1876            Some(data.len() as u64),
1877            &specific,
1878            &extra,
1879            data,
1880        )?);
1881        Ok(())
1882    } else {
1883        write_block(
1884            out,
1885            HEAD_FILE,
1886            block_flags,
1887            Some(data.len() as u64),
1888            &specific,
1889            &extra,
1890            data,
1891        )
1892    }
1893}
1894
1895fn write_stored_entry_fragment(
1896    out: &mut Vec<u8>,
1897    entry: &StoredEntry<'_>,
1898    data: &[u8],
1899    unpacked_size: u64,
1900    data_crc32: Option<u32>,
1901    split_before: bool,
1902    split_after: bool,
1903) -> Result<()> {
1904    let mut extra = Vec::new();
1905    if !split_before && !split_after {
1906        write_hash_record(&mut extra, data);
1907    }
1908    let specific = stored_file_specific(
1909        entry.name,
1910        unpacked_size,
1911        data_crc32,
1912        entry.attributes,
1913        entry.mtime,
1914        entry.host_os,
1915    )?;
1916    let mut block_flags = HFL_DATA;
1917    if split_before {
1918        block_flags |= HFL_SPLIT_BEFORE;
1919    }
1920    if split_after {
1921        block_flags |= HFL_SPLIT_AFTER;
1922    }
1923    if !extra.is_empty() {
1924        block_flags |= HFL_EXTRA;
1925    }
1926
1927    write_block(
1928        out,
1929        HEAD_FILE,
1930        block_flags,
1931        Some(data.len() as u64),
1932        &specific,
1933        &extra,
1934        data,
1935    )
1936}
1937
1938fn write_stored_service(out: &mut Vec<u8>, name: &[u8], data: &[u8]) -> Result<()> {
1939    let mut extra = Vec::new();
1940    write_extra_record(&mut extra, FHEXTRA_SUBDATA, &[]);
1941    let specific = stored_file_specific(name, data.len() as u64, Some(crc32(data)), 0, None, 0)?;
1942
1943    write_block(
1944        out,
1945        HEAD_SERVICE,
1946        HFL_EXTRA | HFL_DATA,
1947        Some(data.len() as u64),
1948        &specific,
1949        &extra,
1950        data,
1951    )
1952}
1953
1954fn write_recovery_service(out: &mut Vec<u8>, recovery_percent: u64) -> Result<()> {
1955    let mut service_data = Vec::new();
1956    write_vint(&mut service_data, recovery_percent);
1957    let mut extra = Vec::new();
1958    write_extra_record(&mut extra, FHEXTRA_SUBDATA, &service_data);
1959
1960    let data = build_structural_inline_recovery_data(out, recovery_percent)?;
1961    let specific = stored_file_specific(b"RR", data.len() as u64, Some(crc32(&data)), 0, None, 0)?;
1962    write_block(
1963        out,
1964        HEAD_SERVICE,
1965        HFL_EXTRA | HFL_DATA,
1966        Some(data.len() as u64),
1967        &specific,
1968        &extra,
1969        &data,
1970    )
1971}
1972
1973fn write_stored_service_with_cache(
1974    out: &mut Vec<u8>,
1975    cached_headers: &mut Vec<CachedHeader>,
1976    name: &[u8],
1977    data: &[u8],
1978) -> Result<()> {
1979    let mut extra = Vec::new();
1980    write_extra_record(&mut extra, FHEXTRA_SUBDATA, &[]);
1981    let specific = stored_file_specific(name, data.len() as u64, Some(crc32(data)), 0, None, 0)?;
1982
1983    write_block_with_cache(
1984        out,
1985        cached_headers,
1986        BlockParts {
1987            header_type: HEAD_SERVICE,
1988            flags: HFL_EXTRA | HFL_DATA,
1989            data_size: Some(data.len() as u64),
1990            type_specific: &specific,
1991            extra: &extra,
1992            data,
1993        },
1994    )
1995}
1996
1997fn write_encrypted_stored_service_with_header_keys(
1998    out: &mut Vec<u8>,
1999    name: &[u8],
2000    comment: EncryptedArchiveCommentEntry<'_>,
2001    header_keys: Option<&Rar50Keys>,
2002) -> Result<()> {
2003    write_encrypted_service_with_header_keys(
2004        out,
2005        name,
2006        comment.data,
2007        &[],
2008        comment.password,
2009        header_keys,
2010    )
2011}
2012
2013fn write_header_encrypted_recovery_service(
2014    out: &mut Vec<u8>,
2015    recovery_percent: u64,
2016    header_keys: &Rar50Keys,
2017) -> Result<()> {
2018    let mut service_data = Vec::new();
2019    write_vint(&mut service_data, recovery_percent);
2020    let data = build_structural_inline_recovery_data(out, recovery_percent)?;
2021    let mut extra = Vec::new();
2022    write_extra_record(&mut extra, FHEXTRA_SUBDATA, &service_data);
2023    let specific = stored_file_specific(b"RR", data.len() as u64, Some(crc32(&data)), 0, None, 0)?;
2024    out.extend_from_slice(&encrypted_header_block(
2025        header_keys,
2026        HEAD_SERVICE,
2027        HFL_EXTRA | HFL_DATA,
2028        Some(data.len() as u64),
2029        &specific,
2030        &extra,
2031        &data,
2032    )?);
2033    Ok(())
2034}
2035
2036fn write_encrypted_service_with_header_keys(
2037    out: &mut Vec<u8>,
2038    name: &[u8],
2039    data: &[u8],
2040    service_data: &[u8],
2041    password: &[u8],
2042    header_keys: Option<&Rar50Keys>,
2043) -> Result<()> {
2044    validate_nonempty_password(password)?;
2045    let encrypted = encrypted_stored_payload(data, password)?;
2046    let mut extra = Vec::new();
2047    write_extra_record(&mut extra, FHEXTRA_SUBDATA, service_data);
2048    write_file_encryption_record(
2049        &mut extra,
2050        encrypted.salt,
2051        encrypted.iv,
2052        encrypted.check_value,
2053    );
2054    write_hash_record_with_value(&mut extra, encrypted.blake2sp_mac);
2055    let specific = stored_file_specific(
2056        name,
2057        data.len() as u64,
2058        Some(encrypted.crc32_mac),
2059        0,
2060        None,
2061        0,
2062    )?;
2063
2064    if let Some(header_keys) = header_keys {
2065        out.extend_from_slice(&encrypted_header_block(
2066            header_keys,
2067            HEAD_SERVICE,
2068            HFL_EXTRA | HFL_DATA,
2069            Some(encrypted.data.len() as u64),
2070            &specific,
2071            &extra,
2072            &encrypted.data,
2073        )?);
2074        Ok(())
2075    } else {
2076        write_block(
2077            out,
2078            HEAD_SERVICE,
2079            HFL_EXTRA | HFL_DATA,
2080            Some(encrypted.data.len() as u64),
2081            &specific,
2082            &extra,
2083            &encrypted.data,
2084        )
2085    }
2086}
2087
2088fn stored_file_specific(
2089    name: &[u8],
2090    unpacked_size: u64,
2091    data_crc32: Option<u32>,
2092    attributes: u64,
2093    mtime: Option<u32>,
2094    host_os: u64,
2095) -> Result<Vec<u8>> {
2096    file_specific(
2097        name,
2098        unpacked_size,
2099        data_crc32,
2100        attributes,
2101        mtime,
2102        0,
2103        host_os,
2104    )
2105}
2106
2107fn file_specific(
2108    name: &[u8],
2109    unpacked_size: u64,
2110    data_crc32: Option<u32>,
2111    attributes: u64,
2112    mtime: Option<u32>,
2113    compression_info: u64,
2114    host_os: u64,
2115) -> Result<Vec<u8>> {
2116    if name.is_empty() {
2117        return Err(Error::InvalidHeader("RAR 5 file name is empty"));
2118    }
2119    let mut file_flags = if data_crc32.is_some() { FHFL_CRC32 } else { 0 };
2120    if mtime.is_some() {
2121        file_flags |= FHFL_MTIME;
2122    }
2123
2124    let mut specific = Vec::new();
2125    write_vint(&mut specific, file_flags);
2126    write_vint(&mut specific, unpacked_size);
2127    write_vint(&mut specific, attributes);
2128    if let Some(mtime) = mtime {
2129        specific.extend_from_slice(&mtime.to_le_bytes());
2130    }
2131    if let Some(data_crc32) = data_crc32 {
2132        specific.extend_from_slice(&data_crc32.to_le_bytes());
2133    }
2134    write_vint(&mut specific, compression_info);
2135    write_vint(&mut specific, host_os);
2136    write_vint(&mut specific, name.len() as u64);
2137    specific.extend_from_slice(name);
2138    Ok(specific)
2139}
2140
2141fn validate_entry(entry: &StoredEntry<'_>) -> Result<()> {
2142    validate_file_entry(entry.name)
2143}
2144
2145fn validate_compressed_entry(entry: &CompressedEntry<'_>) -> Result<()> {
2146    validate_file_entry(entry.name)
2147}
2148
2149fn validate_encrypted_entry(entry: &EncryptedStoredEntry<'_>) -> Result<()> {
2150    validate_file_entry(entry.name)?;
2151    if entry.password.is_empty() {
2152        return Err(Error::InvalidHeader(
2153            "RAR 5 encrypted writer needs a non-empty password",
2154        ));
2155    }
2156    Ok(())
2157}
2158
2159fn validate_encrypted_compressed_entry(entry: &EncryptedCompressedEntry<'_>) -> Result<()> {
2160    validate_file_entry(entry.name)?;
2161    if entry.password.is_empty() {
2162        return Err(Error::InvalidHeader(
2163            "RAR 5 encrypted writer needs a non-empty password",
2164        ));
2165    }
2166    Ok(())
2167}
2168
2169fn validate_file_service(service: &StoredServiceEntry<'_>) -> Result<()> {
2170    if !matches!(service.name, b"ACL" | b"STM" | b"CMT") {
2171        return Err(Error::UnsupportedFeature {
2172            version: crate::ArchiveVersion::Rar50,
2173            feature: "RAR 5 stored file service name",
2174        });
2175    }
2176    if service.data.is_empty() {
2177        return Err(Error::InvalidHeader(
2178            "RAR 5 stored file service data is empty",
2179        ));
2180    }
2181    Ok(())
2182}
2183
2184fn validate_encrypted_file_service(service: &EncryptedStoredServiceEntry<'_>) -> Result<()> {
2185    if !matches!(service.name, b"CMT") {
2186        return Err(Error::UnsupportedFeature {
2187            version: crate::ArchiveVersion::Rar50,
2188            feature: "RAR 5 encrypted stored file service name",
2189        });
2190    }
2191    if service.data.is_empty() {
2192        return Err(Error::InvalidHeader(
2193            "RAR 5 encrypted stored file service data is empty",
2194        ));
2195    }
2196    validate_nonempty_password(service.password)
2197}
2198
2199fn validate_recovery_percent(percent: u64) -> Result<()> {
2200    if !(1..=100).contains(&percent) {
2201        return Err(Error::InvalidHeader(
2202            "RAR 5 recovery percent must be in 1..=100",
2203        ));
2204    }
2205    Ok(())
2206}
2207
2208fn validate_nonempty_password(password: &[u8]) -> Result<()> {
2209    if password.is_empty() {
2210        return Err(Error::InvalidHeader(
2211            "RAR 5 encrypted writer needs a non-empty password",
2212        ));
2213    }
2214    Ok(())
2215}
2216
2217fn validate_file_entry(name: &[u8]) -> Result<()> {
2218    if name.is_empty() {
2219        return Err(Error::InvalidHeader("RAR 5 file name is empty"));
2220    }
2221    Ok(())
2222}
2223
2224fn write_block(
2225    out: &mut Vec<u8>,
2226    header_type: u64,
2227    flags: u64,
2228    data_size: Option<u64>,
2229    type_specific: &[u8],
2230    extra: &[u8],
2231    data: &[u8],
2232) -> Result<()> {
2233    let header = block_header_image(header_type, flags, data_size, type_specific, extra)?;
2234    out.extend_from_slice(&header);
2235    out.extend_from_slice(data);
2236    Ok(())
2237}
2238
2239fn write_block_with_cache(
2240    out: &mut Vec<u8>,
2241    cached_headers: &mut Vec<CachedHeader>,
2242    parts: BlockParts<'_>,
2243) -> Result<()> {
2244    let offset = out.len();
2245    let header = block_header_image(
2246        parts.header_type,
2247        parts.flags,
2248        parts.data_size,
2249        parts.type_specific,
2250        parts.extra,
2251    )?;
2252    cached_headers.push(CachedHeader {
2253        offset,
2254        header: header.clone(),
2255    });
2256    out.extend_from_slice(&header);
2257    out.extend_from_slice(parts.data);
2258    Ok(())
2259}
2260
2261fn quick_open_payload(cached_headers: &[CachedHeader], qo_pos: usize) -> Result<Vec<u8>> {
2262    let mut out = Vec::new();
2263    for cached in cached_headers {
2264        let offset = qo_pos
2265            .checked_sub(cached.offset)
2266            .ok_or(Error::InvalidHeader(
2267                "RAR 5 quick-open cached header is after QO header",
2268            ))?;
2269        let mut body = Vec::new();
2270        write_vint(&mut body, 0);
2271        write_vint(&mut body, offset as u64);
2272        write_vint(&mut body, cached.header.len() as u64);
2273        body.extend_from_slice(&cached.header);
2274
2275        out.extend_from_slice(&crc32(&body).to_le_bytes());
2276        write_vint(&mut out, body.len() as u64);
2277        out.extend_from_slice(&body);
2278    }
2279    Ok(out)
2280}
2281
2282fn encrypted_header_block(
2283    keys: &Rar50Keys,
2284    header_type: u64,
2285    flags: u64,
2286    data_size: Option<u64>,
2287    type_specific: &[u8],
2288    extra: &[u8],
2289    data: &[u8],
2290) -> Result<Vec<u8>> {
2291    let header = block_header_image(header_type, flags, data_size, type_specific, extra)?;
2292    let mut iv = [0u8; 16];
2293    getrandom::fill(&mut iv)
2294        .map_err(|_| Error::InvalidHeader("RAR 5 writer could not generate encryption IV"))?;
2295    let padded_len = header.len().checked_add(15).ok_or(Error::InvalidHeader(
2296        "RAR 5 encrypted header size overflows",
2297    ))? & !15;
2298    let mut encrypted_header = header;
2299    encrypted_header.resize(padded_len, 0);
2300    Rar50Cipher::new(keys.key, iv)
2301        .encrypt_in_place(&mut encrypted_header)
2302        .map_err(super::map_rar50_crypto_error)?;
2303    let mut out = Vec::with_capacity(16 + encrypted_header.len() + data.len());
2304    out.extend_from_slice(&iv);
2305    out.extend_from_slice(&encrypted_header);
2306    out.extend_from_slice(data);
2307    Ok(out)
2308}
2309
2310fn block_header_image(
2311    header_type: u64,
2312    flags: u64,
2313    data_size: Option<u64>,
2314    type_specific: &[u8],
2315    extra: &[u8],
2316) -> Result<Vec<u8>> {
2317    let mut body = Vec::new();
2318    write_vint(&mut body, header_type);
2319    write_vint(&mut body, flags);
2320    if flags & HFL_EXTRA != 0 {
2321        write_vint(&mut body, extra.len() as u64);
2322    }
2323    if let Some(data_size) = data_size {
2324        write_vint(&mut body, data_size);
2325    }
2326    body.extend_from_slice(type_specific);
2327    body.extend_from_slice(extra);
2328
2329    let mut header_size = Vec::new();
2330    write_vint(&mut header_size, body.len() as u64);
2331
2332    let mut header = Vec::with_capacity(4 + header_size.len() + body.len());
2333    header.extend_from_slice(&0u32.to_le_bytes());
2334    header.extend_from_slice(&header_size);
2335    header.extend_from_slice(&body);
2336    let header_crc = crc32(&header[4..]);
2337    header[..4].copy_from_slice(&header_crc.to_le_bytes());
2338    Ok(header)
2339}
2340
2341fn write_extra_record(out: &mut Vec<u8>, record_type: u64, data: &[u8]) {
2342    let mut body = Vec::new();
2343    write_vint(&mut body, record_type);
2344    body.extend_from_slice(data);
2345    write_vint(out, body.len() as u64);
2346    out.extend_from_slice(&body);
2347}
2348
2349fn write_hash_record(out: &mut Vec<u8>, data: &[u8]) {
2350    write_hash_record_with_value(out, blake2sp::hash(data));
2351}
2352
2353fn write_hash_record_with_value(out: &mut Vec<u8>, hash: [u8; 32]) {
2354    let mut record = Vec::new();
2355    write_vint(&mut record, 0);
2356    record.extend_from_slice(&hash);
2357    write_extra_record(out, FHEXTRA_HASH, &record);
2358}
2359
2360fn write_file_encryption_record(
2361    out: &mut Vec<u8>,
2362    salt: [u8; 16],
2363    iv: [u8; 16],
2364    check_value: [u8; 12],
2365) {
2366    let mut record = Vec::new();
2367    write_vint(&mut record, 0);
2368    write_vint(&mut record, 0x0003);
2369    record.push(0);
2370    record.extend_from_slice(&salt);
2371    record.extend_from_slice(&iv);
2372    record.extend_from_slice(&check_value);
2373    write_extra_record(out, FHEXTRA_CRYPT, &record);
2374}
2375
2376fn write_vint(out: &mut Vec<u8>, mut value: u64) {
2377    while value >= 0x80 {
2378        out.push((value as u8) | 0x80);
2379        value >>= 7;
2380    }
2381    out.push(value as u8);
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386    use super::filter_policy::{
2387        auto_delta_filter_range, disjoint_filter_ranges, encode_member_with_auto_size_filter,
2388        encode_member_with_filter_spec, encode_member_with_filter_specs,
2389    };
2390    use super::*;
2391    use crate::x86_filter_scan::auto_x86_filter_ranges;
2392    use rars_codec::rar50::{encode_literal_only, encode_lz_member};
2393    use rars_codec::rar50::{encode_lz_member_with_options, EncodeOptions, Rar50FilterSpec};
2394    use std::cell::RefCell;
2395    use std::fs;
2396    use std::io::{Result as IoResult, Write};
2397    use std::process::Command;
2398    use std::rc::Rc;
2399
2400    struct CollectWriter(Rc<RefCell<Vec<u8>>>);
2401
2402    impl Write for CollectWriter {
2403        fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
2404            self.0.borrow_mut().extend_from_slice(buf);
2405            Ok(buf.len())
2406        }
2407
2408        fn flush(&mut self) -> IoResult<()> {
2409            Ok(())
2410        }
2411    }
2412
2413    #[derive(Debug, Clone, PartialEq, Eq)]
2414    struct CollectedEntry {
2415        name: Vec<u8>,
2416        data: Vec<u8>,
2417        file_time: u32,
2418        attr: u64,
2419        host_os: u64,
2420        is_directory: bool,
2421    }
2422
2423    fn collect_extract(archive: &Archive) -> Result<Vec<CollectedEntry>> {
2424        let entries = RefCell::new(Vec::new());
2425        archive.extract_to(crate::ArchiveReadOptions::default(), |meta| {
2426            let data = Rc::new(RefCell::new(Vec::new()));
2427            entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
2428            Ok(Box::new(CollectWriter(data)))
2429        })?;
2430        Ok(entries
2431            .into_inner()
2432            .into_iter()
2433            .map(|(meta, data)| CollectedEntry {
2434                name: meta.name,
2435                data: data.borrow().clone(),
2436                file_time: meta.file_time,
2437                attr: meta.attr,
2438                host_os: meta.host_os,
2439                is_directory: meta.is_directory,
2440            })
2441            .collect())
2442    }
2443
2444    #[test]
2445    fn writer_plan_offset_resolution_errors_if_offset_never_converges() {
2446        let mut offsets = Vec::new();
2447        let result = resolve_writer_plan_offset(
2448            |offset| {
2449                offsets.push(offset);
2450                Ok((Vec::new(), Some(offset + 1)))
2451            },
2452            "missing offset",
2453            "did not converge",
2454        );
2455
2456        assert!(matches!(
2457            result,
2458            Err(Error::InvalidHeader("did not converge"))
2459        ));
2460        assert_eq!(offsets, [0, 1, 2, 3]);
2461    }
2462
2463    #[test]
2464    fn writer_plan_offset_resolution_rejects_missing_reported_offset() {
2465        let result = resolve_writer_plan_offset(
2466            |_| Ok((Vec::new(), None)),
2467            "missing offset",
2468            "did not converge",
2469        );
2470
2471        assert!(matches!(
2472            result,
2473            Err(Error::InvalidHeader("missing offset"))
2474        ));
2475    }
2476
2477    #[test]
2478    fn internal_literal_only_compressed_member_round_trips_through_rar50_reader() {
2479        let data = b"RAR5 literal-only compressed format-layer experiment\n";
2480        let packed = encode_literal_only(data, 0).unwrap();
2481        let name = b"compressed.txt";
2482
2483        let mut archive = Vec::new();
2484        archive.extend_from_slice(RAR50_SIGNATURE);
2485        write_main_header(&mut archive, 0, None, &[]).unwrap();
2486
2487        let mut extra = Vec::new();
2488        write_hash_record(&mut extra, data);
2489        let compression_info = 1 << 7; // RAR5 v0, non-solid, method m1, 128 KiB dictionary.
2490        let specific = file_specific(
2491            name,
2492            data.len() as u64,
2493            Some(crc32(data)),
2494            0x20,
2495            None,
2496            compression_info,
2497            0,
2498        )
2499        .unwrap();
2500        write_block(
2501            &mut archive,
2502            HEAD_FILE,
2503            HFL_EXTRA | HFL_DATA,
2504            Some(packed.len() as u64),
2505            &specific,
2506            &extra,
2507            &packed,
2508        )
2509        .unwrap();
2510        write_block(&mut archive, HEAD_END, 0, None, &[], &[], &[]).unwrap();
2511
2512        let parsed = Archive::parse(&archive).unwrap();
2513        let file = parsed.files().next().unwrap();
2514        let info = file.decoded_compression_info().unwrap();
2515        assert_eq!(info.method, 1);
2516        assert_eq!(info.dictionary_size, 128 * 1024);
2517
2518        let extracted = collect_extract(&parsed).unwrap();
2519        assert_eq!(extracted[0].name, name);
2520        assert_eq!(extracted[0].data, data);
2521    }
2522
2523    #[test]
2524    fn writer_stamps_requested_rar50_dictionary_size() {
2525        let data = b"RAR5 dictionary-size writer option fixture".repeat(64);
2526        let options =
2527            WriterOptions::new(crate::ArchiveVersion::Rar50, crate::FeatureSet::default())
2528                .with_dictionary_size(512 * 1024);
2529        let entries = [CompressedEntry {
2530            name: b"dict.bin",
2531            data: &data,
2532            mtime: None,
2533            attributes: 0x20,
2534            host_os: 3,
2535        }];
2536        let archive = Rar50Writer::new(options)
2537            .compressed_entries(&entries)
2538            .finish()
2539            .unwrap();
2540
2541        let parsed = Archive::parse(&archive).unwrap();
2542        let info = parsed
2543            .files()
2544            .next()
2545            .unwrap()
2546            .decoded_compression_info()
2547            .unwrap();
2548        let extracted = collect_extract(&parsed).unwrap();
2549
2550        assert_eq!(info.algorithm_version, 0);
2551        assert_eq!(info.dictionary_size, 512 * 1024);
2552        assert_eq!(extracted[0].data, data);
2553    }
2554
2555    #[test]
2556    fn writer_uses_rar7_dictionary_fields_when_size_needs_v1_encoding() {
2557        let data = b"RAR7 dictionary-size writer option fixture".repeat(64);
2558        let options =
2559            WriterOptions::new(crate::ArchiveVersion::Rar70, crate::FeatureSet::default())
2560                .with_dictionary_size(192 * 1024);
2561        let entries = [CompressedEntry {
2562            name: b"dict7.bin",
2563            data: &data,
2564            mtime: None,
2565            attributes: 0x20,
2566            host_os: 3,
2567        }];
2568        let archive = Rar50Writer::new(options)
2569            .compressed_entries(&entries)
2570            .finish()
2571            .unwrap();
2572
2573        let parsed = Archive::parse(&archive).unwrap();
2574        let info = parsed
2575            .files()
2576            .next()
2577            .unwrap()
2578            .decoded_compression_info()
2579            .unwrap();
2580        let extracted = collect_extract(&parsed).unwrap();
2581
2582        assert_eq!(info.algorithm_version, 1);
2583        assert_eq!(info.dictionary_size, 192 * 1024);
2584        assert_eq!(extracted[0].data, data);
2585    }
2586
2587    #[test]
2588    fn writer_rejects_unencodable_rar50_dictionary_size() {
2589        let options =
2590            WriterOptions::new(crate::ArchiveVersion::Rar50, crate::FeatureSet::default())
2591                .with_dictionary_size(192 * 1024);
2592        let entries = [CompressedEntry {
2593            name: b"bad.bin",
2594            data: b"data data data data",
2595            mtime: None,
2596            attributes: 0x20,
2597            host_os: 3,
2598        }];
2599
2600        assert!(matches!(
2601            Rar50Writer::new(options)
2602                .compressed_entries(&entries)
2603                .finish(),
2604            Err(Error::InvalidHeader(
2605                "RAR 5 v0 dictionary size must be a power-of-two multiple of 128 KiB"
2606            ))
2607        ));
2608    }
2609
2610    #[test]
2611    fn non_solid_level_five_considers_lower_level_parse_fallbacks() {
2612        let long_tail = b"stable long match payload for RAR5 best-level search ".repeat(10);
2613        let mut data = Vec::new();
2614        data.extend_from_slice(b"abc");
2615        data.extend_from_slice(&long_tail);
2616        for index in 0..320usize {
2617            data.extend_from_slice(b"abc");
2618            data.push((index as u8).wrapping_mul(37));
2619            data.extend_from_slice(b" near same-hash decoy ");
2620            data.extend_from_slice(&(index as u32).to_le_bytes());
2621        }
2622        data.extend_from_slice(b"abc");
2623        data.extend_from_slice(&long_tail);
2624
2625        let level_five = encode_options_for_level(Some(5), DEFAULT_RAR50_DICTIONARY_SIZE).unwrap();
2626        let fallback_candidates =
2627            encode_option_candidates_for_level(Some(5), DEFAULT_RAR50_DICTIONARY_SIZE).unwrap();
2628        assert!(fallback_candidates.len() > 1);
2629
2630        let level_five_only =
2631            encode_member_with_filter_policy(&data, 0, FilterPolicy::None, level_five).unwrap();
2632        let chosen = encode_member_with_filter_policy_candidates(
2633            &data,
2634            0,
2635            FilterPolicy::None,
2636            &fallback_candidates,
2637        )
2638        .unwrap();
2639
2640        assert!(
2641            chosen.len() <= level_five_only.len(),
2642            "candidate fallback should not choose a larger parse: level5={} chosen={}",
2643            level_five_only.len(),
2644            chosen.len()
2645        );
2646
2647        let mut decoder = rars_codec::rar50::Unpack50Decoder::new();
2648        let output = decoder
2649            .decode_member(
2650                &chosen,
2651                0,
2652                data.len(),
2653                false,
2654                rars_codec::rar50::DecodeMode::Lz,
2655            )
2656            .unwrap();
2657        assert_eq!(output, data);
2658    }
2659
2660    #[test]
2661    fn auto_x86_filter_ranges_select_dense_opcode_clusters() {
2662        let mut data = vec![0u8; 100_000];
2663        data[1_000] = 0xe8;
2664        data[7_000] = 0xe9;
2665        for pos in [50_000, 50_064, 50_128, 50_192] {
2666            data[pos] = 0xe8;
2667        }
2668        for pos in [70_000, 70_064, 70_128, 70_192] {
2669            data[pos] = 0xe9;
2670        }
2671
2672        let e8_ranges = auto_x86_filter_ranges(&data, false);
2673        assert!(e8_ranges
2674            .iter()
2675            .any(|range| range.start <= 50_000 && range.end >= 50_197));
2676        assert!(!e8_ranges
2677            .iter()
2678            .any(|range| range.start <= 1_000 && range.end >= 1_005));
2679
2680        let e8e9_ranges = auto_x86_filter_ranges(&data, true);
2681        assert!(e8e9_ranges
2682            .iter()
2683            .any(|range| range.start <= 70_000 && range.end >= 70_197));
2684    }
2685
2686    #[test]
2687    fn auto_x86_filter_policy_can_emit_multiple_disjoint_ranges() {
2688        let mut data = vec![0x41u8; 80_000];
2689        for cluster_start in [8_000, 60_000] {
2690            for index in 0..8 {
2691                let pos = cluster_start + index * 64;
2692                data[pos] = 0xe8;
2693                data[pos + 1..pos + 5].copy_from_slice(&(0x2000u32 + index as u32).to_le_bytes());
2694            }
2695        }
2696        let ranges = disjoint_filter_ranges(auto_x86_filter_ranges(&data, false));
2697        let filters: Vec<_> = ranges
2698            .into_iter()
2699            .map(|range| Rar50FilterSpec::range(FilterKind::E8, range))
2700            .collect();
2701
2702        let packed =
2703            encode_member_with_filter_specs(&data, 0, &filters, EncodeOptions::default()).unwrap();
2704        let mut decoder = rars_codec::rar50::Unpack50Decoder::new();
2705        let output = decoder
2706            .decode_member(
2707                &packed,
2708                0,
2709                data.len(),
2710                false,
2711                rars_codec::rar50::DecodeMode::Lz,
2712            )
2713            .unwrap();
2714
2715        assert_eq!(filters.len(), 2);
2716        assert_eq!(output, data);
2717    }
2718
2719    #[test]
2720    fn auto_delta_filter_range_skips_container_edges_and_aligns_channels() {
2721        let data = vec![0u8; 512];
2722
2723        let range = auto_delta_filter_range(&data, 3).unwrap();
2724
2725        assert!(range.start >= AUTO_DELTA_EDGE_SKIP);
2726        assert!(range.end <= data.len() - AUTO_DELTA_EDGE_SKIP);
2727        assert_eq!(range.start % 3, 0);
2728        assert_eq!((range.end - range.start) % 3, 0);
2729        assert!(auto_delta_filter_range(&data[..80], 3).is_none());
2730    }
2731
2732    #[test]
2733    fn auto_filter_policy_considers_ranged_delta_candidates() {
2734        let mut data = vec![0x55u8; AUTO_DELTA_EDGE_SKIP];
2735        for sample in 0..256u16 {
2736            let left = sample as u8;
2737            let right = left.wrapping_add(1);
2738            data.extend_from_slice(&[left, right]);
2739        }
2740        data.extend(std::iter::repeat_n(0xaa, AUTO_DELTA_EDGE_SKIP));
2741        let options = EncodeOptions::default();
2742
2743        let plain = encode_lz_member_with_options(&data, 0, options).unwrap();
2744        let ranged = encode_member_with_filter_spec(
2745            &data,
2746            0,
2747            Rar50FilterSpec::range(
2748                FilterKind::Delta { channels: 2 },
2749                auto_delta_filter_range(&data, 2).unwrap(),
2750            ),
2751            options,
2752        )
2753        .unwrap();
2754        let auto = encode_member_with_auto_size_filter(&data, 0, options).unwrap();
2755
2756        assert!(ranged.len() < plain.len());
2757        assert!(auto.len() <= ranged.len());
2758        let mut decoder = rars_codec::rar50::Unpack50Decoder::new();
2759        let output = decoder
2760            .decode_member(
2761                &auto,
2762                0,
2763                data.len(),
2764                false,
2765                rars_codec::rar50::DecodeMode::Lz,
2766            )
2767            .unwrap();
2768        assert_eq!(output, data);
2769    }
2770
2771    #[test]
2772    fn explicit_filters_accept_large_members_after_filter_ranges_are_split() {
2773        let data = vec![0u8; 4 * 1024 * 1024 + 1];
2774        let packed = encode_member_with_filter_policy(
2775            &data,
2776            0,
2777            FilterPolicy::Explicit(FilterKind::Delta { channels: 1 }),
2778            EncodeOptions::new(0),
2779        )
2780        .unwrap();
2781        let mut decoder = rars_codec::rar50::Unpack50Decoder::new();
2782
2783        assert_eq!(
2784            decoder
2785                .decode_member(
2786                    &packed,
2787                    0,
2788                    data.len(),
2789                    false,
2790                    rars_codec::rar50::DecodeMode::Lz
2791                )
2792                .unwrap(),
2793            data
2794        );
2795    }
2796
2797    #[test]
2798    fn solid_reset_policy_chooses_smaller_of_continued_and_fresh_streams() {
2799        let options = EncodeOptions::default();
2800        let first = b"solid reset policy unrelated prefix data\n".repeat(32);
2801        let second = b"second member second member second member\n".repeat(16);
2802        let mut encoder = Unpack50Encoder::with_options(options);
2803        encoder.encode_member(&first, 0).unwrap();
2804
2805        let mut continued = encoder.clone();
2806        let continued_packed = continued.encode_member(&second, 0).unwrap();
2807        let mut fresh = Unpack50Encoder::with_options(options);
2808        let fresh_packed = fresh.encode_member(&second, 0).unwrap();
2809        let expected_fresh = fresh_packed.len() < continued_packed.len();
2810        let expected_len = continued_packed.len().min(fresh_packed.len());
2811
2812        let (packed, solid_continuation) =
2813            encode_with_solid_reset_policy(&mut encoder, &second, 0, options, 1).unwrap();
2814
2815        assert_eq!(packed.len(), expected_len);
2816        assert_eq!(solid_continuation, !expected_fresh);
2817    }
2818
2819    #[test]
2820    #[ignore = "requires local rar command; used for reference-validating experimental RAR5 compressed output"]
2821    fn reference_rar_accepts_internal_literal_only_compressed_member() {
2822        let data = b"RAR5 literal-only compressed reference experiment\n";
2823        let packed = encode_literal_only(data, 0).unwrap();
2824        let name = b"compressed.txt";
2825
2826        let mut archive = Vec::new();
2827        archive.extend_from_slice(RAR50_SIGNATURE);
2828        write_main_header(&mut archive, 0, None, &[]).unwrap();
2829
2830        let mut extra = Vec::new();
2831        write_hash_record(&mut extra, data);
2832        let specific = file_specific(
2833            name,
2834            data.len() as u64,
2835            Some(crc32(data)),
2836            0x20,
2837            None,
2838            1 << 7,
2839            0,
2840        )
2841        .unwrap();
2842        write_block(
2843            &mut archive,
2844            HEAD_FILE,
2845            HFL_EXTRA | HFL_DATA,
2846            Some(packed.len() as u64),
2847            &specific,
2848            &extra,
2849            &packed,
2850        )
2851        .unwrap();
2852        write_block(&mut archive, HEAD_END, 0, None, &[], &[], &[]).unwrap();
2853
2854        let mut path = std::env::temp_dir();
2855        path.push(format!(
2856            "rars-rar50-literal-only-{}.rar",
2857            std::process::id()
2858        ));
2859        fs::write(&path, archive).unwrap();
2860        let output = match Command::new("rar").arg("t").arg(&path).output() {
2861            Ok(output) => output,
2862            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
2863                eprintln!("skipping reference test: local `rar` command is not installed");
2864                return;
2865            }
2866            Err(error) => panic!("failed to run rar: {error}"),
2867        };
2868        if std::env::var_os("RARS_KEEP_REFERENCE_ARCHIVE").is_none() {
2869            let _ = fs::remove_file(&path);
2870        }
2871
2872        assert!(
2873            output.status.success(),
2874            "rar rejected experimental RAR5 compressed output\nstdout:\n{}\nstderr:\n{}",
2875            String::from_utf8_lossy(&output.stdout),
2876            String::from_utf8_lossy(&output.stderr)
2877        );
2878    }
2879
2880    #[test]
2881    #[ignore = "requires local rar command; used for reference-validating experimental RAR5 match output"]
2882    fn reference_rar_accepts_internal_match_compressed_member() {
2883        let data = b"RAR5 match compressed reference experiment\n".repeat(8);
2884        let packed = encode_lz_member(&data, 0).unwrap();
2885        let name = b"compressed.txt";
2886
2887        let mut archive = Vec::new();
2888        archive.extend_from_slice(RAR50_SIGNATURE);
2889        write_main_header(&mut archive, 0, None, &[]).unwrap();
2890
2891        let mut extra = Vec::new();
2892        write_hash_record(&mut extra, &data);
2893        let specific = file_specific(
2894            name,
2895            data.len() as u64,
2896            Some(crc32(&data)),
2897            0x20,
2898            None,
2899            1 << 7,
2900            0,
2901        )
2902        .unwrap();
2903        write_block(
2904            &mut archive,
2905            HEAD_FILE,
2906            HFL_EXTRA | HFL_DATA,
2907            Some(packed.len() as u64),
2908            &specific,
2909            &extra,
2910            &packed,
2911        )
2912        .unwrap();
2913        write_block(&mut archive, HEAD_END, 0, None, &[], &[], &[]).unwrap();
2914
2915        let mut path = std::env::temp_dir();
2916        path.push(format!("rars-rar50-match-{}.rar", std::process::id()));
2917        fs::write(&path, archive).unwrap();
2918        let output = match Command::new("rar").arg("t").arg(&path).output() {
2919            Ok(output) => output,
2920            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
2921                eprintln!("skipping reference test: local `rar` command is not installed");
2922                return;
2923            }
2924            Err(error) => panic!("failed to run rar: {error}"),
2925        };
2926        if std::env::var_os("RARS_KEEP_REFERENCE_ARCHIVE").is_none() {
2927            let _ = fs::remove_file(&path);
2928        }
2929
2930        assert!(
2931            output.status.success(),
2932            "rar rejected experimental RAR5 match output\nstdout:\n{}\nstderr:\n{}",
2933            String::from_utf8_lossy(&output.stdout),
2934            String::from_utf8_lossy(&output.stderr)
2935        );
2936    }
2937
2938    #[test]
2939    fn writer_options_default_targets_rar50_with_store_only_features() {
2940        let options = WriterOptions::default();
2941        assert_eq!(options.target, crate::ArchiveVersion::Rar50);
2942        assert_eq!(options.features, crate::FeatureSet::store_only());
2943    }
2944
2945    #[test]
2946    fn writer_rejects_mixed_member_kinds_without_panicking() {
2947        let stored = [StoredEntry {
2948            name: b"stored.txt",
2949            data: b"stored",
2950            mtime: None,
2951            attributes: 0x20,
2952            host_os: 3,
2953        }];
2954        let compressed = [CompressedEntry {
2955            name: b"compressed.txt",
2956            data: b"compressed compressed compressed",
2957            mtime: None,
2958            attributes: 0x20,
2959            host_os: 3,
2960        }];
2961        let result = Rar50Writer::new(WriterOptions::new(
2962            crate::ArchiveVersion::Rar50,
2963            crate::FeatureSet::store_only(),
2964        ))
2965        .stored_entries(&stored)
2966        .compressed_entries(&compressed)
2967        .finish();
2968
2969        assert!(matches!(
2970            result,
2971            Err(Error::UnsupportedFeature {
2972                version: crate::ArchiveVersion::Rar50,
2973                feature: "RAR 5 mixed stored/compressed writer plan",
2974            })
2975        ));
2976    }
2977}