esplugin/
record_id.rs

1use std::cmp::Ordering;
2/*
3 * This file is part of esplugin
4 *
5 * Copyright (C) 2017 Oliver Hamlet
6 *
7 * esplugin is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * esplugin is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with esplugin. If not, see <http://www.gnu.org/licenses/>.
19 */
20use std::collections::hash_map::DefaultHasher;
21use std::collections::HashSet;
22use std::hash::{Hash, Hasher};
23use std::ops::RangeInclusive;
24
25pub(crate) enum RecordId {
26    FormId(std::num::NonZeroU32),
27    NamespacedId(NamespacedId),
28}
29
30/// This is a FormID equivalent for Morrowind plugin records.
31/// Record IDs with the same data in the same namespace refer to the same record
32/// but if the data or namespace is different, the IDs refer to different records.
33#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
34pub(crate) struct NamespacedId {
35    namespace: Namespace,
36    hashed_id: u64,
37}
38
39impl NamespacedId {
40    pub(crate) fn new(record_type: [u8; 4], id_data: &[u8]) -> Self {
41        let mut hasher = DefaultHasher::new();
42        id_data.hash(&mut hasher);
43
44        Self {
45            namespace: record_type.into(),
46            hashed_id: hasher.finish(),
47        }
48    }
49}
50
51/// Each record's ID belongs to a namespace, depending on the record type.
52/// Some record types share the same namespace, others have their own unique
53/// namespace.
54/// Information about namespaces from <https://github.com/loot/loot/issues/1101#issuecomment-480629856>
55#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
56pub enum Namespace {
57    Race,
58    Class,
59    Birthsign,
60    Script,
61    Cell,
62    Faction,
63    Sound,
64    Global,
65    Region,
66    Skill,
67    MagicEffect,
68    Land,
69    PathGrid,
70    Dialog,
71    Other,
72}
73
74impl From<[u8; 4]> for Namespace {
75    fn from(record_type: [u8; 4]) -> Namespace {
76        match &record_type {
77            b"RACE" => Self::Race,
78            b"CLAS" => Self::Class,
79            b"BSGN" => Self::Birthsign,
80            b"SCPT" => Self::Script,
81            b"CELL" => Self::Cell,
82            b"FACT" => Self::Faction,
83            b"SOUN" => Self::Sound,
84            b"GLOB" => Self::Global,
85            b"REGN" => Self::Region,
86            b"SKIL" => Self::Skill,
87            b"MGEF" => Self::MagicEffect,
88            b"LAND" => Self::Land,
89            b"PGRD" => Self::PathGrid,
90            b"DIAL" => Self::Dialog,
91            _ => Self::Other,
92        }
93    }
94}
95
96impl From<Namespace> for u32 {
97    #[expect(
98        clippy::as_conversions,
99        reason = "No better way to convert unit enum variant to its value"
100    )]
101    fn from(value: Namespace) -> u32 {
102        value as u32
103    }
104}
105
106#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
107pub enum ObjectIndexMask {
108    Full = 0x00FF_FFFF,
109    Medium = 0x0000_FFFF,
110    Small = 0x0000_0FFF,
111}
112
113impl From<ObjectIndexMask> for u32 {
114    #[expect(
115        clippy::as_conversions,
116        reason = "No better way to convert unit enum variant to its value"
117    )]
118    fn from(value: ObjectIndexMask) -> u32 {
119        value as u32
120    }
121}
122
123#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
124pub(crate) struct SourcePlugin {
125    pub hashed_name: u64,
126    /// mod_index_mask is not used when the SourcePlugin is used to represent the plugin that a FormID is found in.
127    pub mod_index_mask: u32,
128    pub object_index_mask: u32,
129}
130
131impl SourcePlugin {
132    pub(crate) fn master(
133        name: &str,
134        mod_index_mask: u32,
135        object_index_mask: ObjectIndexMask,
136    ) -> Self {
137        let object_index_mask = u32::from(object_index_mask);
138
139        SourcePlugin {
140            hashed_name: calculate_filename_hash(name),
141            mod_index_mask,
142            object_index_mask,
143        }
144    }
145
146    pub(crate) fn parent(name: &str, object_index_mask: ObjectIndexMask) -> Self {
147        let object_index_mask = u32::from(object_index_mask);
148
149        // Set mod_index_mask to object_index_mask because it should be unused but needs a value, and object_index_mask is obviously wrong (if a parent SourcePlugin is used as a master SourcePlugin, it'll never match any of the plugin's raw FormIDs).
150        SourcePlugin {
151            hashed_name: calculate_filename_hash(name),
152            mod_index_mask: object_index_mask,
153            object_index_mask,
154        }
155    }
156}
157
158#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
159pub(crate) enum RecordIdType {
160    FormId,
161    NamespacedId,
162}
163
164#[derive(Clone, Debug)]
165pub(crate) struct ResolvedRecordId {
166    record_id_type: RecordIdType,
167    overridden_record: bool,
168    hashed_data: u64,
169    other_data: u32,
170}
171
172impl ResolvedRecordId {
173    pub(crate) fn from_form_id(
174        parent_plugin: SourcePlugin,
175        masters: &[SourcePlugin],
176        raw_form_id: u32,
177    ) -> Self {
178        let source_master = masters
179            .iter()
180            .find(|m| (raw_form_id & !m.object_index_mask) == m.mod_index_mask);
181
182        if let Some(hashed_master) = source_master {
183            let object_index = raw_form_id & hashed_master.object_index_mask;
184            ResolvedRecordId {
185                record_id_type: RecordIdType::FormId,
186                overridden_record: true,
187                hashed_data: hashed_master.hashed_name,
188                other_data: object_index,
189            }
190        } else {
191            let object_index = raw_form_id & parent_plugin.object_index_mask;
192            ResolvedRecordId {
193                record_id_type: RecordIdType::FormId,
194                overridden_record: false,
195                hashed_data: parent_plugin.hashed_name,
196                other_data: object_index,
197            }
198        }
199    }
200
201    pub(crate) fn from_namespaced_id(
202        namespaced_id: &NamespacedId,
203        masters_record_ids: &HashSet<NamespacedId>,
204    ) -> Self {
205        let overridden_record = masters_record_ids.contains(namespaced_id);
206
207        ResolvedRecordId {
208            record_id_type: RecordIdType::NamespacedId,
209            overridden_record,
210            hashed_data: namespaced_id.hashed_id,
211            other_data: namespaced_id.namespace.into(),
212        }
213    }
214
215    pub(crate) fn is_overridden_record(&self) -> bool {
216        self.overridden_record
217    }
218
219    pub(crate) fn is_object_index_in(&self, range: &RangeInclusive<u32>) -> bool {
220        match self.record_id_type {
221            RecordIdType::FormId => range.contains(&self.other_data),
222            RecordIdType::NamespacedId => false,
223        }
224    }
225}
226
227impl Ord for ResolvedRecordId {
228    fn cmp(&self, other: &Self) -> Ordering {
229        match self.record_id_type.cmp(&other.record_id_type) {
230            Ordering::Equal => match self.other_data.cmp(&other.other_data) {
231                Ordering::Equal => self.hashed_data.cmp(&other.hashed_data),
232                o => o,
233            },
234            o => o,
235        }
236    }
237}
238
239impl PartialOrd for ResolvedRecordId {
240    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
241        Some(self.cmp(other))
242    }
243}
244
245impl PartialEq for ResolvedRecordId {
246    fn eq(&self, other: &Self) -> bool {
247        self.record_id_type == other.record_id_type
248            && self.other_data == other.other_data
249            && self.hashed_data == other.hashed_data
250    }
251}
252
253impl Eq for ResolvedRecordId {}
254
255impl Hash for ResolvedRecordId {
256    fn hash<H: Hasher>(&self, state: &mut H) {
257        self.record_id_type.hash(state);
258        self.other_data.hash(state);
259        self.hashed_data.hash(state);
260    }
261}
262
263pub(crate) fn calculate_filename_hash(string: &str) -> u64 {
264    let mut hasher = DefaultHasher::new();
265    string.to_lowercase().hash(&mut hasher);
266    hasher.finish()
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    const OTHER_RECORD_TYPES: &[&[u8; 4]] = &[
274        b"ACTI", b"ALCH", b"APPA", b"ARMO", b"BODY", b"BOOK", b"CLOT", b"CONT", b"CREA", b"DOOR",
275        b"ENCH", b"GMST", b"INFO", b"INGR", b"LEVC", b"LEVI", b"LIGH", b"LOCK", b"LTEX", b"MISC",
276        b"NPC_", b"PROB", b"REPA", b"SNDG", b"SPEL", b"STAT", b"TES3", b"WEAP",
277    ];
278
279    #[test]
280    fn namespace_from_array_should_namespace_race_class_bsgn_scpt_cell_fact_soun_glob_and_regn() {
281        assert_eq!(Namespace::Race, (*b"RACE").into());
282        assert_eq!(Namespace::Class, (*b"CLAS").into());
283        assert_eq!(Namespace::Birthsign, (*b"BSGN").into());
284        assert_eq!(Namespace::Script, (*b"SCPT").into());
285        assert_eq!(Namespace::Cell, (*b"CELL").into());
286        assert_eq!(Namespace::Faction, (*b"FACT").into());
287        assert_eq!(Namespace::Sound, (*b"SOUN").into());
288        assert_eq!(Namespace::Global, (*b"GLOB").into());
289        assert_eq!(Namespace::Region, (*b"REGN").into());
290        assert_eq!(Namespace::Skill, (*b"SKIL").into());
291        assert_eq!(Namespace::MagicEffect, (*b"MGEF").into());
292        assert_eq!(Namespace::Land, (*b"LAND").into());
293        assert_eq!(Namespace::PathGrid, (*b"PGRD").into());
294        assert_eq!(Namespace::Dialog, (*b"DIAL").into());
295    }
296
297    #[test]
298    fn namespace_from_array_should_put_unrecognised_record_types_into_other_namespace() {
299        assert_eq!(Namespace::Other, (*b"    ").into());
300        assert_eq!(Namespace::Other, (*b"DUMY").into());
301
302        for record_type in OTHER_RECORD_TYPES {
303            assert_eq!(Namespace::Other, (**record_type).into());
304        }
305    }
306
307    #[test]
308    fn namespaced_id_new_should_hash_id_data_and_map_record_type_to_namespace() {
309        let data = vec![1, 2, 3, 4];
310        let record_id = NamespacedId::new(*b"BOOK", &data);
311
312        let mut hasher = DefaultHasher::new();
313        data.hash(&mut hasher);
314        let hashed_data = hasher.finish();
315
316        assert_eq!(Namespace::Other, record_id.namespace);
317        assert_eq!(hashed_data, record_id.hashed_id);
318    }
319
320    mod source_plugin {
321        use super::*;
322
323        #[test]
324        fn parent_should_use_object_index_mask_as_mod_index_mask() {
325            let plugin = SourcePlugin::parent("a", ObjectIndexMask::Full);
326
327            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
328            assert_eq!(plugin.mod_index_mask, plugin.object_index_mask);
329        }
330    }
331
332    #[expect(clippy::as_conversions, reason = "Unavoidable in const expressions")]
333    mod resolved_record_id {
334        use super::*;
335
336        const PARENT_PLUGIN_NAME: u64 = 1;
337        const PARENT_PLUGIN: SourcePlugin = SourcePlugin {
338            hashed_name: PARENT_PLUGIN_NAME,
339            mod_index_mask: ObjectIndexMask::Full as u32,
340            object_index_mask: ObjectIndexMask::Full as u32,
341        };
342        const OTHER_PARENT_PLUGIN: SourcePlugin = SourcePlugin {
343            hashed_name: 6,
344            mod_index_mask: ObjectIndexMask::Full as u32,
345            object_index_mask: ObjectIndexMask::Full as u32,
346        };
347        const MASTERS: &[SourcePlugin] = &[
348            SourcePlugin {
349                hashed_name: 2,
350                mod_index_mask: 0,
351                object_index_mask: ObjectIndexMask::Full as u32,
352            },
353            SourcePlugin {
354                hashed_name: 3,
355                mod_index_mask: 0x1200_0000,
356                object_index_mask: ObjectIndexMask::Full as u32,
357            },
358            SourcePlugin {
359                hashed_name: 4,
360                mod_index_mask: 0xFD12_0000,
361                object_index_mask: ObjectIndexMask::Medium as u32,
362            },
363            SourcePlugin {
364                hashed_name: 5,
365                mod_index_mask: 0xFE12_3000,
366                object_index_mask: ObjectIndexMask::Small as u32,
367            },
368        ];
369        const NO_MASTERS: &[SourcePlugin] = &[];
370
371        fn hash(form_id: &ResolvedRecordId) -> u64 {
372            let mut hasher = DefaultHasher::new();
373            form_id.hash(&mut hasher);
374            hasher.finish()
375        }
376
377        #[test]
378        fn new_should_match_override_record_to_master_based_on_mod_index() {
379            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x0045_6789);
380
381            assert!(form_id.is_overridden_record());
382            assert_eq!(0x0045_6789, form_id.other_data);
383            assert_eq!(MASTERS[0].hashed_name, form_id.hashed_data);
384
385            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x1245_6789);
386
387            assert!(form_id.is_overridden_record());
388            assert_eq!(0x0045_6789, form_id.other_data);
389            assert_eq!(MASTERS[1].hashed_name, form_id.hashed_data);
390
391            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFD12_6789);
392
393            assert!(form_id.is_overridden_record());
394            assert_eq!(0x6789, form_id.other_data);
395            assert_eq!(MASTERS[2].hashed_name, form_id.hashed_data);
396
397            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFE12_3789);
398
399            assert!(form_id.is_overridden_record());
400            assert_eq!(0x789, form_id.other_data);
401            assert_eq!(MASTERS[3].hashed_name, form_id.hashed_data);
402        }
403
404        #[test]
405        fn new_should_create_non_override_formid_if_no_master_mod_indexes_match() {
406            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x0145_6789);
407
408            assert!(!form_id.is_overridden_record());
409            assert_eq!(0x0045_6789, form_id.other_data);
410            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
411
412            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x2045_6789);
413
414            assert!(!form_id.is_overridden_record());
415            assert_eq!(0x0045_6789, form_id.other_data);
416            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
417
418            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFD21_6789);
419
420            assert!(!form_id.is_overridden_record());
421            assert_eq!(0x0021_6789, form_id.other_data);
422            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
423
424            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFE32_1789);
425
426            assert!(!form_id.is_overridden_record());
427            assert_eq!(0x0032_1789, form_id.other_data);
428            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
429        }
430
431        #[test]
432        fn new_should_use_parent_source_plugin_other_data_mask_if_no_master_mod_indexes_match() {
433            let parent_plugin = SourcePlugin {
434                hashed_name: PARENT_PLUGIN_NAME,
435                mod_index_mask: u32::from(ObjectIndexMask::Full),
436                object_index_mask: u32::from(ObjectIndexMask::Full),
437            };
438            let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0x0145_6789);
439
440            assert!(!form_id.is_overridden_record());
441            assert_eq!(0x0045_6789, form_id.other_data);
442            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
443
444            let parent_plugin = SourcePlugin {
445                hashed_name: PARENT_PLUGIN_NAME,
446                mod_index_mask: u32::from(ObjectIndexMask::Medium),
447                object_index_mask: u32::from(ObjectIndexMask::Medium),
448            };
449            let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0xFD21_6789);
450
451            assert!(!form_id.is_overridden_record());
452            assert_eq!(0x6789, form_id.other_data);
453            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
454
455            let parent_plugin = SourcePlugin {
456                hashed_name: PARENT_PLUGIN_NAME,
457                mod_index_mask: u32::from(ObjectIndexMask::Small),
458                object_index_mask: u32::from(ObjectIndexMask::Small),
459            };
460            let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0xFE32_1789);
461
462            assert!(!form_id.is_overridden_record());
463            assert_eq!(0x789, form_id.other_data);
464            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
465        }
466
467        #[test]
468        fn form_ids_should_not_be_equal_if_plugin_names_are_unequal() {
469            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
470            let form_id2 =
471                ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x0500_0001);
472
473            assert_ne!(form_id1, form_id2);
474        }
475
476        #[test]
477        fn form_ids_should_not_be_equal_if_object_indices_are_unequal() {
478            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
479            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
480
481            assert_ne!(form_id1, form_id2);
482        }
483
484        #[test]
485        fn form_ids_with_equal_plugin_names_and_object_ids_should_be_equal() {
486            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, NO_MASTERS, 0x01);
487            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x0500_0001);
488
489            assert_eq!(form_id1, form_id2);
490        }
491
492        #[test]
493        fn form_ids_can_be_equal_if_one_is_an_override_record_and_the_other_is_not() {
494            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
495            let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x0500_0001);
496
497            assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
498            assert_eq!(form_id1, form_id2);
499        }
500
501        #[test]
502        fn form_ids_should_be_ordered_according_to_object_index_then_hashed_datas() {
503            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
504            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
505
506            assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
507            assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));
508
509            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x0500_0001);
510            let form_id2 =
511                ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x0500_0001);
512
513            assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
514            assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));
515        }
516
517        #[test]
518        fn form_ids_should_not_be_ordered_according_to_override_record_flag_value() {
519            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
520            let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x0500_0001);
521
522            assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
523            assert_eq!(Ordering::Equal, form_id2.cmp(&form_id1));
524        }
525
526        #[test]
527        fn form_id_hashes_should_not_be_equal_if_plugin_names_are_unequal() {
528            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
529            let form_id2 =
530                ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x0500_0001);
531
532            assert_ne!(hash(&form_id1), hash(&form_id2));
533        }
534
535        #[test]
536        fn form_id_hashes_should_not_be_equal_if_object_indices_are_unequal() {
537            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
538            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
539
540            assert_ne!(hash(&form_id1), hash(&form_id2));
541        }
542
543        #[test]
544        fn form_id_hashes_with_equal_plugin_names_and_object_ids_should_be_equal() {
545            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, NO_MASTERS, 0x01);
546            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x0500_0001);
547
548            assert_eq!(hash(&form_id1), hash(&form_id2));
549        }
550
551        #[test]
552        fn form_id_hashes_can_be_equal_with_unequal_override_record_flag_values() {
553            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
554            let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x0500_0001);
555
556            assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
557            assert_eq!(hash(&form_id1), hash(&form_id2));
558        }
559    }
560
561    #[test]
562    fn calculate_filename_hash_should_treat_plugin_names_case_insensitively_like_windows() {
563        // \u03a1 is greek rho uppercase 'Ρ'
564        // \u03c1 is greek rho lowercase 'ρ'
565        // \u03f1 is greek rho 'ϱ'
566        // \u0130 is turkish 'İ'
567        // \u0131 is turkish 'ı'
568
569        // I and i are the same, but İ and ı are different to them and each
570        // other.
571        assert_eq!(calculate_filename_hash("i"), calculate_filename_hash("I"));
572        assert_ne!(
573            calculate_filename_hash("\u{0130}"),
574            calculate_filename_hash("i")
575        );
576        assert_ne!(
577            calculate_filename_hash("\u{0131}"),
578            calculate_filename_hash("i")
579        );
580        assert_ne!(
581            calculate_filename_hash("\u{0131}"),
582            calculate_filename_hash("\u{0130}")
583        );
584
585        // Windows filesystem treats Ρ and ρ as the same, but ϱ is different.
586        assert_eq!(
587            calculate_filename_hash("\u{03a1}"),
588            calculate_filename_hash("\u{03c1}")
589        );
590        assert_ne!(
591            calculate_filename_hash("\u{03a1}"),
592            calculate_filename_hash("\u{03f1}")
593        );
594
595        // hash uses str::to_lowercase() internally, because unlike
596        // str::to_uppercase() it has the desired behaviour. The asserts below
597        // demonstrate that.
598
599        // Check how greek rho Ρ case transforms.
600        assert_eq!("\u{03c1}", "\u{03a1}".to_lowercase());
601        assert_eq!("\u{03a1}", "\u{03a1}".to_uppercase());
602
603        // Check how greek rho ρ case transforms.
604        assert_eq!("\u{03c1}", "\u{03c1}".to_lowercase());
605        assert_eq!("\u{03a1}", "\u{03c1}".to_uppercase());
606
607        // Check how greek rho ϱ case transforms.
608        assert_eq!("\u{03f1}", "\u{03f1}".to_lowercase());
609        assert_eq!("\u{03a1}", "\u{03f1}".to_uppercase());
610
611        // Check how turkish İ case transforms.
612        assert_eq!("i\u{0307}", "\u{0130}".to_lowercase());
613        assert_eq!("\u{0130}", "\u{0130}".to_uppercase());
614
615        // Check how turkish ı case transforms.
616        assert_eq!("\u{0131}", "\u{0131}".to_lowercase());
617        assert_eq!("I", "\u{0131}".to_uppercase());
618    }
619}