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 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 struct NamespacedId {
35    namespace: Namespace,
36    hashed_id: u64,
37}
38
39impl NamespacedId {
40    pub 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        use self::Namespace::*;
77        match &record_type {
78            b"RACE" => Race,
79            b"CLAS" => Class,
80            b"BSGN" => Birthsign,
81            b"SCPT" => Script,
82            b"CELL" => Cell,
83            b"FACT" => Faction,
84            b"SOUN" => Sound,
85            b"GLOB" => Global,
86            b"REGN" => Region,
87            b"SKIL" => Skill,
88            b"MGEF" => MagicEffect,
89            b"LAND" => Land,
90            b"PGRD" => PathGrid,
91            b"DIAL" => Dialog,
92            _ => Other,
93        }
94    }
95}
96
97impl From<Namespace> for u32 {
98    fn from(value: Namespace) -> u32 {
99        value as u32
100    }
101}
102
103#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
104pub enum ObjectIndexMask {
105    Full = 0x00FF_FFFF,
106    Medium = 0x0000_FFFF,
107    Small = 0x0000_0FFF,
108}
109
110impl From<ObjectIndexMask> for u32 {
111    fn from(value: ObjectIndexMask) -> u32 {
112        value as u32
113    }
114}
115
116#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
117pub struct SourcePlugin {
118    pub hashed_name: u64,
119    /// mod_index_mask is not used when the SourcePlugin is used to represent the plugin that a FormID is found in.
120    pub mod_index_mask: u32,
121    pub object_index_mask: u32,
122}
123
124impl SourcePlugin {
125    pub fn master(name: &str, mod_index_mask: u32, object_index_mask: ObjectIndexMask) -> Self {
126        let object_index_mask = u32::from(object_index_mask);
127
128        SourcePlugin {
129            hashed_name: calculate_filename_hash(name),
130            mod_index_mask,
131            object_index_mask,
132        }
133    }
134
135    pub fn parent(name: &str, object_index_mask: ObjectIndexMask) -> Self {
136        let object_index_mask = u32::from(object_index_mask);
137
138        // 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).
139        SourcePlugin {
140            hashed_name: calculate_filename_hash(name),
141            mod_index_mask: object_index_mask,
142            object_index_mask,
143        }
144    }
145}
146
147#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
148pub enum RecordIdType {
149    FormId,
150    NamespacedId,
151}
152
153#[derive(Clone, Debug)]
154pub struct ResolvedRecordId {
155    record_id_type: RecordIdType,
156    overridden_record: bool,
157    hashed_data: u64,
158    other_data: u32,
159}
160
161impl ResolvedRecordId {
162    pub fn from_form_id(
163        parent_plugin: SourcePlugin,
164        masters: &[SourcePlugin],
165        raw_form_id: u32,
166    ) -> Self {
167        let source_master = masters
168            .iter()
169            .find(|m| (raw_form_id & !m.object_index_mask) == m.mod_index_mask);
170
171        match source_master {
172            Some(hashed_master) => {
173                let object_index = raw_form_id & hashed_master.object_index_mask;
174                ResolvedRecordId {
175                    record_id_type: RecordIdType::FormId,
176                    overridden_record: true,
177                    hashed_data: hashed_master.hashed_name,
178                    other_data: object_index,
179                }
180            }
181            None => {
182                let object_index = raw_form_id & parent_plugin.object_index_mask;
183                ResolvedRecordId {
184                    record_id_type: RecordIdType::FormId,
185                    overridden_record: false,
186                    hashed_data: parent_plugin.hashed_name,
187                    other_data: object_index,
188                }
189            }
190        }
191    }
192
193    pub fn from_namespaced_id(
194        namespaced_id: &NamespacedId,
195        masters_record_ids: &HashSet<NamespacedId>,
196    ) -> Self {
197        let overridden_record = masters_record_ids.contains(namespaced_id);
198
199        ResolvedRecordId {
200            record_id_type: RecordIdType::NamespacedId,
201            overridden_record,
202            hashed_data: namespaced_id.hashed_id,
203            other_data: namespaced_id.namespace.into(),
204        }
205    }
206
207    pub fn is_overridden_record(&self) -> bool {
208        self.overridden_record
209    }
210
211    pub fn is_object_index_in(&self, range: &RangeInclusive<u32>) -> bool {
212        match self.record_id_type {
213            RecordIdType::FormId => range.contains(&self.other_data),
214            RecordIdType::NamespacedId => false,
215        }
216    }
217}
218
219impl Ord for ResolvedRecordId {
220    fn cmp(&self, other: &Self) -> Ordering {
221        match self.record_id_type.cmp(&other.record_id_type) {
222            Ordering::Equal => match self.other_data.cmp(&other.other_data) {
223                Ordering::Equal => self.hashed_data.cmp(&other.hashed_data),
224                o => o,
225            },
226            o => o,
227        }
228    }
229}
230
231impl PartialOrd for ResolvedRecordId {
232    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
233        Some(self.cmp(other))
234    }
235}
236
237impl PartialEq for ResolvedRecordId {
238    fn eq(&self, other: &Self) -> bool {
239        self.record_id_type == other.record_id_type
240            && self.other_data == other.other_data
241            && self.hashed_data == other.hashed_data
242    }
243}
244
245impl Eq for ResolvedRecordId {}
246
247impl Hash for ResolvedRecordId {
248    fn hash<H: Hasher>(&self, state: &mut H) {
249        self.record_id_type.hash(state);
250        self.other_data.hash(state);
251        self.hashed_data.hash(state);
252    }
253}
254
255pub fn calculate_filename_hash(string: &str) -> u64 {
256    let mut hasher = DefaultHasher::new();
257    string.to_lowercase().hash(&mut hasher);
258    hasher.finish()
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    const OTHER_RECORD_TYPES: &[&[u8; 4]] = &[
266        b"ACTI", b"ALCH", b"APPA", b"ARMO", b"BODY", b"BOOK", b"CLOT", b"CONT", b"CREA", b"DOOR",
267        b"ENCH", b"GMST", b"INFO", b"INGR", b"LEVC", b"LEVI", b"LIGH", b"LOCK", b"LTEX", b"MISC",
268        b"NPC_", b"PROB", b"REPA", b"SNDG", b"SPEL", b"STAT", b"TES3", b"WEAP",
269    ];
270
271    #[test]
272    fn namespace_from_array_should_namespace_race_class_bsgn_scpt_cell_fact_soun_glob_and_regn() {
273        assert_eq!(Namespace::Race, (*b"RACE").into());
274        assert_eq!(Namespace::Class, (*b"CLAS").into());
275        assert_eq!(Namespace::Birthsign, (*b"BSGN").into());
276        assert_eq!(Namespace::Script, (*b"SCPT").into());
277        assert_eq!(Namespace::Cell, (*b"CELL").into());
278        assert_eq!(Namespace::Faction, (*b"FACT").into());
279        assert_eq!(Namespace::Sound, (*b"SOUN").into());
280        assert_eq!(Namespace::Global, (*b"GLOB").into());
281        assert_eq!(Namespace::Region, (*b"REGN").into());
282        assert_eq!(Namespace::Skill, (*b"SKIL").into());
283        assert_eq!(Namespace::MagicEffect, (*b"MGEF").into());
284        assert_eq!(Namespace::Land, (*b"LAND").into());
285        assert_eq!(Namespace::PathGrid, (*b"PGRD").into());
286        assert_eq!(Namespace::Dialog, (*b"DIAL").into());
287    }
288
289    #[test]
290    fn namespace_from_array_should_put_unrecognised_record_types_into_other_namespace() {
291        assert_eq!(Namespace::Other, (*b"    ").into());
292        assert_eq!(Namespace::Other, (*b"DUMY").into());
293
294        for record_type in OTHER_RECORD_TYPES {
295            assert_eq!(Namespace::Other, (**record_type).into());
296        }
297    }
298
299    #[test]
300    fn namespaced_id_new_should_hash_id_data_and_map_record_type_to_namespace() {
301        let data = vec![1, 2, 3, 4];
302        let record_id = NamespacedId::new(*b"BOOK", &data);
303
304        let mut hasher = DefaultHasher::new();
305        data.hash(&mut hasher);
306        let hashed_data = hasher.finish();
307
308        assert_eq!(Namespace::Other, record_id.namespace);
309        assert_eq!(hashed_data, record_id.hashed_id);
310    }
311
312    mod source_plugin {
313        use super::*;
314
315        #[test]
316        fn parent_should_use_object_index_mask_as_mod_index_mask() {
317            let plugin = SourcePlugin::parent("a", ObjectIndexMask::Full);
318
319            assert_eq!(u32::from(ObjectIndexMask::Full), plugin.object_index_mask);
320            assert_eq!(plugin.mod_index_mask, plugin.object_index_mask);
321        }
322    }
323
324    mod resolved_record_id {
325        use super::*;
326
327        const PARENT_PLUGIN_NAME: u64 = 1;
328        const PARENT_PLUGIN: SourcePlugin = SourcePlugin {
329            hashed_name: PARENT_PLUGIN_NAME,
330            mod_index_mask: ObjectIndexMask::Full as u32,
331            object_index_mask: ObjectIndexMask::Full as u32,
332        };
333        const OTHER_PARENT_PLUGIN: SourcePlugin = SourcePlugin {
334            hashed_name: 6,
335            mod_index_mask: ObjectIndexMask::Full as u32,
336            object_index_mask: ObjectIndexMask::Full as u32,
337        };
338        const MASTERS: &[SourcePlugin] = &[
339            SourcePlugin {
340                hashed_name: 2,
341                mod_index_mask: 0,
342                object_index_mask: ObjectIndexMask::Full as u32,
343            },
344            SourcePlugin {
345                hashed_name: 3,
346                mod_index_mask: 0x1200_0000,
347                object_index_mask: ObjectIndexMask::Full as u32,
348            },
349            SourcePlugin {
350                hashed_name: 4,
351                mod_index_mask: 0xFD12_0000,
352                object_index_mask: ObjectIndexMask::Medium as u32,
353            },
354            SourcePlugin {
355                hashed_name: 5,
356                mod_index_mask: 0xFE12_3000,
357                object_index_mask: ObjectIndexMask::Small as u32,
358            },
359        ];
360        const NO_MASTERS: &[SourcePlugin] = &[];
361
362        fn hash(form_id: &ResolvedRecordId) -> u64 {
363            let mut hasher = DefaultHasher::new();
364            form_id.hash(&mut hasher);
365            hasher.finish()
366        }
367
368        #[test]
369        fn new_should_match_override_record_to_master_based_on_mod_index() {
370            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x00456789);
371
372            assert!(form_id.is_overridden_record());
373            assert_eq!(0x456789, form_id.other_data);
374            assert_eq!(MASTERS[0].hashed_name, form_id.hashed_data);
375
376            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x12456789);
377
378            assert!(form_id.is_overridden_record());
379            assert_eq!(0x456789, form_id.other_data);
380            assert_eq!(MASTERS[1].hashed_name, form_id.hashed_data);
381
382            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFD126789);
383
384            assert!(form_id.is_overridden_record());
385            assert_eq!(0x6789, form_id.other_data);
386            assert_eq!(MASTERS[2].hashed_name, form_id.hashed_data);
387
388            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFE123789);
389
390            assert!(form_id.is_overridden_record());
391            assert_eq!(0x789, form_id.other_data);
392            assert_eq!(MASTERS[3].hashed_name, form_id.hashed_data);
393        }
394
395        #[test]
396        fn new_should_create_non_override_formid_if_no_master_mod_indexes_match() {
397            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01456789);
398
399            assert!(!form_id.is_overridden_record());
400            assert_eq!(0x456789, form_id.other_data);
401            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
402
403            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x20456789);
404
405            assert!(!form_id.is_overridden_record());
406            assert_eq!(0x456789, form_id.other_data);
407            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
408
409            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFD216789);
410
411            assert!(!form_id.is_overridden_record());
412            assert_eq!(0x216789, form_id.other_data);
413            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
414
415            let form_id = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0xFE321789);
416
417            assert!(!form_id.is_overridden_record());
418            assert_eq!(0x321789, form_id.other_data);
419            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
420        }
421
422        #[test]
423        fn new_should_use_parent_source_plugin_other_data_mask_if_no_master_mod_indexes_match() {
424            let parent_plugin = SourcePlugin {
425                hashed_name: PARENT_PLUGIN_NAME,
426                mod_index_mask: u32::from(ObjectIndexMask::Full),
427                object_index_mask: u32::from(ObjectIndexMask::Full),
428            };
429            let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0x01456789);
430
431            assert!(!form_id.is_overridden_record());
432            assert_eq!(0x456789, form_id.other_data);
433            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
434
435            let parent_plugin = SourcePlugin {
436                hashed_name: PARENT_PLUGIN_NAME,
437                mod_index_mask: u32::from(ObjectIndexMask::Medium),
438                object_index_mask: u32::from(ObjectIndexMask::Medium),
439            };
440            let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0xFD216789);
441
442            assert!(!form_id.is_overridden_record());
443            assert_eq!(0x6789, form_id.other_data);
444            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
445
446            let parent_plugin = SourcePlugin {
447                hashed_name: PARENT_PLUGIN_NAME,
448                mod_index_mask: u32::from(ObjectIndexMask::Small),
449                object_index_mask: u32::from(ObjectIndexMask::Small),
450            };
451            let form_id = ResolvedRecordId::from_form_id(parent_plugin, MASTERS, 0xFE321789);
452
453            assert!(!form_id.is_overridden_record());
454            assert_eq!(0x789, form_id.other_data);
455            assert_eq!(PARENT_PLUGIN_NAME, form_id.hashed_data);
456        }
457
458        #[test]
459        fn form_ids_should_not_be_equal_if_plugin_names_are_unequal() {
460            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
461            let form_id2 = ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x05000001);
462
463            assert_ne!(form_id1, form_id2);
464        }
465
466        #[test]
467        fn form_ids_should_not_be_equal_if_object_indices_are_unequal() {
468            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
469            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
470
471            assert_ne!(form_id1, form_id2);
472        }
473
474        #[test]
475        fn form_ids_with_equal_plugin_names_and_object_ids_should_be_equal() {
476            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, NO_MASTERS, 0x01);
477            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x05000001);
478
479            assert_eq!(form_id1, form_id2);
480        }
481
482        #[test]
483        fn form_ids_can_be_equal_if_one_is_an_override_record_and_the_other_is_not() {
484            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
485            let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x05000001);
486
487            assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
488            assert_eq!(form_id1, form_id2);
489        }
490
491        #[test]
492        fn form_ids_should_be_ordered_according_to_object_index_then_hashed_datas() {
493            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
494            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
495
496            assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
497            assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));
498
499            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x05000001);
500            let form_id2 = ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x05000001);
501
502            assert_eq!(Ordering::Less, form_id1.cmp(&form_id2));
503            assert_eq!(Ordering::Greater, form_id2.cmp(&form_id1));
504        }
505
506        #[test]
507        fn form_ids_should_not_be_ordered_according_to_override_record_flag_value() {
508            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
509            let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x05000001);
510
511            assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
512            assert_eq!(Ordering::Equal, form_id2.cmp(&form_id1));
513        }
514
515        #[test]
516        fn form_id_hashes_should_not_be_equal_if_plugin_names_are_unequal() {
517            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
518            let form_id2 = ResolvedRecordId::from_form_id(OTHER_PARENT_PLUGIN, MASTERS, 0x05000001);
519
520            assert_ne!(hash(&form_id1), hash(&form_id2));
521        }
522
523        #[test]
524        fn form_id_hashes_should_not_be_equal_if_object_indices_are_unequal() {
525            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
526            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x02);
527
528            assert_ne!(hash(&form_id1), hash(&form_id2));
529        }
530
531        #[test]
532        fn form_id_hashes_with_equal_plugin_names_and_object_ids_should_be_equal() {
533            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, NO_MASTERS, 0x01);
534            let form_id2 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x05000001);
535
536            assert_eq!(hash(&form_id1), hash(&form_id2));
537        }
538
539        #[test]
540        fn form_id_hashes_can_be_equal_with_unequal_override_record_flag_values() {
541            let form_id1 = ResolvedRecordId::from_form_id(PARENT_PLUGIN, MASTERS, 0x01);
542            let form_id2 = ResolvedRecordId::from_form_id(MASTERS[0], NO_MASTERS, 0x05000001);
543
544            assert_ne!(form_id1.overridden_record, form_id2.overridden_record);
545            assert_eq!(hash(&form_id1), hash(&form_id2));
546        }
547    }
548
549    #[test]
550    fn calculate_filename_hash_should_treat_plugin_names_case_insensitively_like_windows() {
551        // \u03a1 is greek rho uppercase 'Ρ'
552        // \u03c1 is greek rho lowercase 'ρ'
553        // \u03f1 is greek rho 'ϱ'
554        // \u0130 is turkish 'İ'
555        // \u0131 is turkish 'ı'
556
557        // I and i are the same, but İ and ı are different to them and each
558        // other.
559        assert_eq!(calculate_filename_hash("i"), calculate_filename_hash("I"));
560        assert_ne!(
561            calculate_filename_hash("\u{0130}"),
562            calculate_filename_hash("i")
563        );
564        assert_ne!(
565            calculate_filename_hash("\u{0131}"),
566            calculate_filename_hash("i")
567        );
568        assert_ne!(
569            calculate_filename_hash("\u{0131}"),
570            calculate_filename_hash("\u{0130}")
571        );
572
573        // Windows filesystem treats Ρ and ρ as the same, but ϱ is different.
574        assert_eq!(
575            calculate_filename_hash("\u{03a1}"),
576            calculate_filename_hash("\u{03c1}")
577        );
578        assert_ne!(
579            calculate_filename_hash("\u{03a1}"),
580            calculate_filename_hash("\u{03f1}")
581        );
582
583        // hash uses str::to_lowercase() internally, because unlike
584        // str::to_uppercase() it has the desired behaviour. The asserts below
585        // demonstrate that.
586
587        // Check how greek rho Ρ case transforms.
588        assert_eq!("\u{03c1}", "\u{03a1}".to_lowercase());
589        assert_eq!("\u{03a1}", "\u{03a1}".to_uppercase());
590
591        // Check how greek rho ρ case transforms.
592        assert_eq!("\u{03c1}", "\u{03c1}".to_lowercase());
593        assert_eq!("\u{03a1}", "\u{03c1}".to_uppercase());
594
595        // Check how greek rho ϱ case transforms.
596        assert_eq!("\u{03f1}", "\u{03f1}".to_lowercase());
597        assert_eq!("\u{03a1}", "\u{03f1}".to_uppercase());
598
599        // Check how turkish İ case transforms.
600        assert_eq!("i\u{0307}", "\u{0130}".to_lowercase());
601        assert_eq!("\u{0130}", "\u{0130}".to_uppercase());
602
603        // Check how turkish ı case transforms.
604        assert_eq!("\u{0131}", "\u{0131}".to_lowercase());
605        assert_eq!("I", "\u{0131}".to_uppercase());
606    }
607}