unity_version/version/
mod.rs

1use crate::sys::version as version_impl;
2use derive_more::Display;
3use nom::{
4    branch::alt,
5    character::complete::{char, digit1, hex_digit1, space1},
6    combinator::{map_res, verify},
7    error::context,
8    sequence::delimited,
9    IResult, Parser,
10};
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use std::path::{Path, PathBuf};
13use std::{cmp::Ordering, str::FromStr};
14use regex::Regex;
15
16mod release_type;
17mod revision_hash;
18use crate::error::VersionError;
19pub use release_type::ReleaseType;
20pub use revision_hash::RevisionHash;
21
22#[derive(Eq, Debug, Clone, Hash, PartialOrd, Display)]
23#[display("{}{}{}", base, release_type, revision)]
24pub struct Version {
25    base: semver::Version,
26    release_type: ReleaseType,
27    revision: u64,
28}
29
30impl Serialize for Version {
31    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
32    where
33        S: Serializer,
34    {
35        let s = self.to_string();
36        serializer.serialize_str(&s)
37    }
38}
39
40impl<'de> Deserialize<'de> for Version {
41    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42    where
43        D: Deserializer<'de>,
44    {
45        let s = String::deserialize(deserializer)?;
46        Version::from_str(&s).map_err(serde::de::Error::custom)
47    }
48}
49
50impl Ord for Version {
51    fn cmp(&self, other: &Version) -> Ordering {
52        self.base
53            .cmp(&other.base)
54            .then(self.release_type.cmp(&other.release_type))
55            .then(self.revision.cmp(&other.revision))
56    }
57}
58
59impl PartialEq for Version {
60    fn eq(&self, other: &Self) -> bool {
61        self.base == other.base
62            && self.release_type == other.release_type
63            && self.revision == other.revision
64    }
65}
66
67impl AsRef<Version> for Version {
68    fn as_ref(&self) -> &Self {
69        self
70    }
71}
72
73impl AsMut<Version> for Version {
74    fn as_mut(&mut self) -> &mut Self {
75        self
76    }
77}
78
79impl FromStr for Version {
80    type Err = VersionError;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        match parse_version(s) {
84            Ok((_, version)) => Ok(version),
85            Err(err) => {
86                let error_msg = match err {
87                    nom::Err::Error(e) | nom::Err::Failure(e) => {
88                        format!("Parse error at: {}", e.input)
89                    },
90                    _ => "Unknown parsing error".to_string(),
91                };
92                Err(VersionError::ParsingFailed(error_msg))
93            }
94        }
95    }
96}
97
98impl TryFrom<&str> for Version {
99    type Error = <Version as FromStr>::Err;
100
101    fn try_from(value: &str) -> Result<Self, Self::Error> {
102        Version::from_str(value)
103    }
104}
105
106impl TryFrom<String> for Version {
107    type Error = <Version as FromStr>::Err;
108
109    fn try_from(value: String) -> Result<Self, Self::Error> {
110        Version::from_str(&value)
111    }
112}
113
114impl TryFrom<PathBuf> for Version {
115    type Error = VersionError;
116
117    fn try_from(path: PathBuf) -> Result<Self, VersionError> {
118        Version::from_path(path)
119    }
120}
121
122impl TryFrom<&Path> for Version {
123    type Error = VersionError;
124
125    fn try_from(path: &Path) -> Result<Self, VersionError> {
126        Version::from_path(path)
127    }
128}
129
130impl Version {
131    pub fn new(
132        major: u64,
133        minor: u64,
134        patch: u64,
135        release_type: ReleaseType,
136        revision: u64,
137    ) -> Version {
138        let base = semver::Version::new(major, minor, patch);
139        Version {
140            base,
141            release_type,
142            revision,
143        }
144    }
145
146    pub fn release_type(&self) -> ReleaseType {
147        self.release_type
148    }
149
150    pub fn major(&self) -> u64 {
151        self.base.major
152    }
153
154    pub fn minor(&self) -> u64 {
155        self.base.minor
156    }
157
158    pub fn patch(&self) -> u64 {
159        self.base.patch
160    }
161
162    pub fn revision(&self) -> u64 {
163        self.revision
164    }
165
166    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, VersionError> {
167        version_impl::read_version_from_path(path)
168    }
169
170    pub fn from_string_containing<S: AsRef<str>>(s: S) -> Result<Self, VersionError> {
171        let s = s.as_ref();
172        Self::extract_version_from_text(s)
173            .ok_or_else(|| VersionError::ParsingFailed(format!("Could not find a valid Unity version in string: {}", s)))
174    }
175
176    /// Extract Unity version from text using prioritized approach.
177    /// Prioritizes versions with hashes (more reliable) over standalone versions.
178    fn extract_version_from_text(text: &str) -> Option<Version> {
179        use std::sync::OnceLock;
180        
181        // Enhanced regex to capture versions with optional hash suffixes
182        static VERSION_REGEX: OnceLock<Regex> = OnceLock::new();
183        let regex = VERSION_REGEX.get_or_init(|| {
184            Regex::new(r"([0-9]{1,4})\.([0-9]{1,4})\.([0-9]{1,4})(f|p|b|a)([0-9]{1,4})(_([a-z0-9]{12})| \(([a-z0-9]{12})\)|/([a-z0-9]{12}))?").unwrap()
185        });
186        
187        // Priority 1: Look for versions with parentheses hash format (most authoritative)
188        for captures in regex.captures_iter(text) {
189            if captures.get(8).is_some() {
190                // This version has a hash in parentheses format: "version (hash)"
191                let version_string = format!(
192                    "{}.{}.{}{}{}",
193                    &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
194                );
195                
196                if let Ok(version) = Version::from_str(&version_string) {
197                    return Some(version);
198                }
199            }
200        }
201        
202        // Priority 2: Look for versions with underscore hash format
203        for captures in regex.captures_iter(text) {
204            if captures.get(7).is_some() {
205                // This version has a hash in underscore format
206                let version_string = format!(
207                    "{}.{}.{}{}{}",
208                    &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
209                );
210                
211                if let Ok(version) = Version::from_str(&version_string) {
212                    return Some(version);
213                }
214            }
215        }
216        
217        // Priority 3: Look for versions with slash hash format
218        for captures in regex.captures_iter(text) {
219            if captures.get(9).is_some() {
220                // This version has a hash in slash format
221                let version_string = format!(
222                    "{}.{}.{}{}{}",
223                    &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
224                );
225                
226                if let Ok(version) = Version::from_str(&version_string) {
227                    return Some(version);
228                }
229            }
230        }
231        
232        // Priority 4: Fallback to any version string found (without hash requirement)
233        for captures in regex.captures_iter(text) {
234            let version_string = format!(
235                "{}.{}.{}{}{}",
236                &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
237            );
238            
239            if let Ok(version) = Version::from_str(&version_string) {
240                return Some(version);
241            }
242        }
243        
244        None
245    }
246
247    pub fn base(&self) -> &semver::Version {
248        &self.base
249    }
250
251    /// Find Unity version by running `strings` on an executable and parsing the output.
252    /// This works on Unix-like systems (Linux, macOS) where the `strings` command is available.
253    #[cfg(unix)]
254    pub fn find_version_in_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, VersionError> {
255        use std::process::{Command, Stdio};
256        use log::debug;
257
258        let path = path.as_ref();
259        debug!("find api version in Unity executable {}", path.display());
260
261        let child = Command::new("strings")
262            .arg("--")
263            .arg(path)
264            .stdout(Stdio::piped())
265            .stderr(Stdio::piped())
266            .spawn()
267            .map_err(|e| VersionError::Other {
268                source: e.into(),
269                msg: "failed to spawn strings".to_string(),
270            })?;
271
272        let output = child.wait_with_output().map_err(|e| VersionError::Other {
273            source: e.into(),
274            msg: "failed to spawn strings".to_string(),
275        })?;
276
277        if !output.status.success() {
278            return Err(VersionError::ExecutableContainsNoVersion(
279                path.to_path_buf(),
280            ));
281        }
282
283        let strings_output = String::from_utf8_lossy(&output.stdout);
284        
285        // Use the shared version extraction logic
286        Self::extract_version_from_text(&strings_output)
287            .map(|version| {
288                debug!("found version {} in executable", &version);
289                version
290            })
291            .ok_or_else(|| VersionError::ExecutableContainsNoVersion(path.to_path_buf()))
292    }
293}
294
295#[derive(Eq, Debug, Clone, Hash, Display)]
296#[display("{} ({})", version, revision)]
297#[allow(dead_code)]
298pub struct CompleteVersion {
299    version: Version,
300    revision: RevisionHash,
301}
302
303impl CompleteVersion {
304    /// Creates a new CompleteVersion from a Version and RevisionHash.
305    pub fn new(version: Version, revision: RevisionHash) -> Self {
306        Self { version, revision }
307    }
308    
309    /// Gets the version component.
310    pub fn version(&self) -> &Version {
311        &self.version
312    }
313    
314    /// Gets the revision hash component.
315    pub fn revision(&self) -> &RevisionHash {
316        &self.revision
317    }
318}
319
320impl FromStr for CompleteVersion {
321    type Err = VersionError;
322
323    /// Parses a complete Unity version string with revision hash.
324    /// 
325    /// # Format
326    /// 
327    /// Expects format: "VERSION (REVISION_HASH)"
328    /// Examples: "2021.3.55f1 (f87d5274e360)", "2022.1.5f1 (abc123def456)"
329    /// 
330    /// # Errors
331    /// 
332    /// Returns an error if:
333    /// - No revision hash is present
334    /// - Version format is invalid  
335    /// - Revision hash format is invalid
336    fn from_str(s: &str) -> Result<Self, Self::Err> {
337        match parse_complete_version(s.trim()) {
338            Ok((remaining, complete_version)) => {
339                if remaining.is_empty() {
340                    Ok(complete_version)
341                } else {
342                    Err(VersionError::ParsingFailed(
343                        format!("Unexpected remaining input: '{}'", remaining)
344                    ))
345                }
346            }
347            Err(err) => {
348                let error_msg = match err {
349                    nom::Err::Error(e) | nom::Err::Failure(e) => {
350                        format!("Parse error at: '{}'", e.input)
351                    }
352                    nom::Err::Incomplete(_) => {
353                        "Incomplete input".to_string()
354                    }
355                };
356                Err(VersionError::ParsingFailed(error_msg))
357            }
358        }
359    }
360}
361
362impl PartialEq for CompleteVersion {
363    fn eq(&self, other: &Self) -> bool {
364        self.version == other.version
365    }
366}
367
368impl PartialOrd for CompleteVersion {
369    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
370        self.version.partial_cmp(&other.version)
371    }
372}
373
374fn parse_release_type(input: &str) -> IResult<&str, ReleaseType> {
375    context(
376        "release type",
377        map_res(alt((char('f'), char('b'), char('a'), char('p'))), |c| {
378            ReleaseType::try_from(c)
379        }),
380    ).parse(input)
381}
382
383fn parse_version(input: &str) -> IResult<&str, Version> {
384    context(
385        "version",
386        (
387            context("major version", map_res(digit1, |s: &str| s.parse::<u64>())),
388            char('.'),
389            context("minor version", map_res(digit1, |s: &str| s.parse::<u64>())),
390            char('.'),
391            context("patch version", map_res(digit1, |s: &str| s.parse::<u64>())),
392            parse_release_type,
393            context("revision", map_res(digit1, |s: &str| s.parse::<u64>())),
394        )
395    )
396    .map(|(major, _, minor, _, patch, release_type, revision)| {
397        let base = semver::Version::new(major, minor, patch);
398        Version {
399            base,
400            release_type,
401            revision,
402        }
403    })
404    .parse(input)
405}
406
407fn parse_revision_hash(input: &str) -> IResult<&str, RevisionHash> {
408    context(
409        "revision hash",
410        map_res(
411            verify(hex_digit1, |s: &str| s.len() == 12),
412            |hex_str: &str| RevisionHash::new(hex_str)
413        )
414    ).parse(input)
415}
416
417fn parse_complete_version(input: &str) -> IResult<&str, CompleteVersion> {
418    context(
419        "complete version",
420        (
421            parse_version,
422            space1,
423            delimited(char('('), parse_revision_hash, char(')')),
424        )
425    )
426    .map(|(version, _, revision)| CompleteVersion::new(version, revision))
427    .parse(input)
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use proptest::prelude::*;
434
435    #[test]
436    fn parse_version_string_with_valid_input() {
437        let version_string = "1.2.3f4";
438        let version = Version::from_str(version_string);
439        assert!(version.is_ok(), "valid input returns a version")
440    }
441
442    #[test]
443    fn splits_version_string_into_components() {
444        let version_string = "11.2.3f4";
445        let version = Version::from_str(version_string).unwrap();
446
447        assert_eq!(version.base.major, 11, "parse correct major component");
448        assert_eq!(version.base.minor, 2, "parse correct minor component");
449        assert_eq!(version.base.patch, 3, "parse correct patch component");
450
451        assert_eq!(version.release_type, ReleaseType::Final);
452        assert_eq!(version.revision, 4, "parse correct revision component");
453    }
454
455    #[test]
456    fn test_complete_version_from_str() {
457        // Test successful parsing
458        let complete_version = CompleteVersion::from_str("2021.3.55f1 (f87d5274e360)").unwrap();
459        assert_eq!(complete_version.version().to_string(), "2021.3.55f1");
460        assert_eq!(complete_version.revision().as_str(), "f87d5274e360");
461        assert_eq!(complete_version.to_string(), "2021.3.55f1 (f87d5274e360)");
462
463        // Test different version formats
464        let alpha_version = CompleteVersion::from_str("2023.1.0a1 (123456789abc)").unwrap();
465        assert_eq!(alpha_version.version().to_string(), "2023.1.0a1");
466        assert_eq!(alpha_version.revision().as_str(), "123456789abc");
467
468        // Test error cases with specific error message validation
469        
470        // No revision hash
471        let no_hash_result = CompleteVersion::from_str("2021.3.55f1");
472        assert!(no_hash_result.is_err());
473        let error_msg = no_hash_result.unwrap_err().to_string();
474        assert!(error_msg.contains("Parse error"), "Expected parsing error for missing hash, got: {}", error_msg);
475        
476        // Invalid version format
477        let invalid_version_result = CompleteVersion::from_str("invalid (f87d5274e360)");
478        assert!(invalid_version_result.is_err());
479        let error_msg = invalid_version_result.unwrap_err().to_string();
480        assert!(error_msg.contains("Parse error"), "Expected parsing error for invalid version, got: {}", error_msg);
481        
482        // Invalid hash characters
483        let invalid_hash_result = CompleteVersion::from_str("2021.3.55f1 (invalid)");
484        assert!(invalid_hash_result.is_err());
485        let error_msg = invalid_hash_result.unwrap_err().to_string();
486        assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"), 
487                "Expected revision hash error, got: {}", error_msg);
488        
489        // Hash too short
490        let short_hash_result = CompleteVersion::from_str("2021.3.55f1 (f87d527)");
491        assert!(short_hash_result.is_err());
492        let error_msg = short_hash_result.unwrap_err().to_string();
493        assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"), 
494                "Expected revision hash error for short hash, got: {}", error_msg);
495        
496        // Hash too long
497        let long_hash_result = CompleteVersion::from_str("2021.3.55f1 (f87d5274e360ab)");
498        assert!(long_hash_result.is_err());
499        let error_msg = long_hash_result.unwrap_err().to_string();
500        assert!(error_msg.contains("Parse error"), "Expected parsing error for long hash, got: {}", error_msg);
501        
502        // Non-hex characters in hash
503        let non_hex_result = CompleteVersion::from_str("2021.3.55f1 (f87d5274e36z)");
504        assert!(non_hex_result.is_err());
505        let error_msg = non_hex_result.unwrap_err().to_string();
506        assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"), 
507                "Expected revision hash error for non-hex chars, got: {}", error_msg);
508    }
509
510    #[test]
511    fn extracts_version_from_text() {
512        let text = "Some text before 2023.1.4f5 and some after";
513        let result = Version::from_string_containing(text);
514        assert!(result.is_ok(), "Should successfully extract the version");
515
516        let version = result.unwrap();
517        assert_eq!(version.base.major, 2023);
518        assert_eq!(version.base.minor, 1);
519        assert_eq!(version.base.patch, 4);
520        assert_eq!(version.release_type, ReleaseType::Final);
521        assert_eq!(version.revision, 5);
522    }
523
524
525    #[test]
526    fn extracts_version_from_text_and_returns_first_complete_version() {
527        let text = "Some text 23 before 2023.1.4f5 and some after";
528        let result = Version::from_string_containing(text);
529        assert!(result.is_ok(), "Should successfully extract the version");
530
531        let version = result.unwrap();
532        assert_eq!(version.base.major, 2023);
533        assert_eq!(version.base.minor, 1);
534        assert_eq!(version.base.patch, 4);
535        assert_eq!(version.release_type, ReleaseType::Final);
536        assert_eq!(version.revision, 5);
537    }
538    /// Generate test data with bogus content and Unity versions in various positions
539    fn generate_test_data_with_versions(
540        parentheses_version: Option<&str>,
541        underscore_version: Option<&str>, 
542        slash_version: Option<&str>,
543        standalone_versions: &[&str],
544        bogus_data_size: usize
545    ) -> String {
546        let mut content = String::new();
547        
548        // Add initial bogus data
549        for i in 0..bogus_data_size / 4 {
550            content.push_str(&format!("__libc_start_main_{}\n", i));
551            content.push_str("malloc\nfree\nstrlen\n");
552            content.push_str("/lib64/ld-linux-x86-64.so.2\n");
553            content.push_str("Some random binary string data\n");
554        }
555        
556        // Add standalone versions scattered throughout
557        for (idx, version) in standalone_versions.iter().enumerate() {
558            if idx % 2 == 0 {
559                content.push_str(&format!("Random text {}\n", idx));
560            }
561            content.push_str(&format!("{}\n", version));
562            content.push_str("More random data\n");
563        }
564        
565        // Add more bogus data
566        for i in 0..bogus_data_size / 4 {
567            content.push_str(&format!("function_name_{}\n", i));
568            content.push_str("symbol_table_entry\n");
569            content.push_str("debug_info_string\n");
570        }
571        
572        // Add slash version if provided
573        if let Some(version) = slash_version {
574            content.push_str("path/to/unity/\n");
575            content.push_str(&format!("{}\n", version));
576            content.push_str("more/path/data\n");
577        }
578        
579        // Add more bogus data
580        for i in 0..bogus_data_size / 4 {
581            content.push_str(&format!("error_message_{}\n", i));
582            content.push_str("log_entry_data\n");
583        }
584        
585        // Add underscore version if provided  
586        if let Some(version) = underscore_version {
587            content.push_str("version_info_block\n");
588            content.push_str(&format!("{}\n", version));
589            content.push_str("build_metadata\n");
590        }
591        
592        // Add final bogus data
593        for i in 0..bogus_data_size / 4 {
594            content.push_str(&format!("final_symbol_{}\n", i));
595            content.push_str("cleanup_data\n");
596        }
597        
598        // Add parentheses version at the end if provided (should still be prioritized)
599        if let Some(version) = parentheses_version {
600            content.push_str("unity_build_info\n");
601            content.push_str(&format!("{}\n", version));
602            content.push_str("end_of_data\n");
603        }
604        
605        content
606    }
607
608    #[test]
609    fn prioritizes_parentheses_hash_over_other_formats_in_large_dataset() {
610        let test_data = generate_test_data_with_versions(
611            Some("2023.1.5f1 (abc123def456)"),
612            Some("2022.3.2f1_xyz789uvw012"), 
613            Some("2021.2.1f1/def456ghi789"),
614            &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1", "2018.3.5f1"],
615            1000  // Large amount of bogus data
616        );
617        
618        let result = Version::from_string_containing(&test_data);
619        assert!(result.is_ok(), "Should extract version from large dataset");
620        
621        let version = result.unwrap();
622        // Should prioritize the parentheses version even though it appears last
623        assert_eq!(version.base.major, 2023);
624        assert_eq!(version.base.minor, 1);
625        assert_eq!(version.base.patch, 5);
626        assert_eq!(version.release_type, ReleaseType::Final);
627        assert_eq!(version.revision, 1);
628    }
629
630    #[test]
631    fn prioritizes_underscore_hash_when_no_parentheses_version() {
632        let test_data = generate_test_data_with_versions(
633            None, // No parentheses version
634            Some("2022.3.2f1_xyz789uvw012"), 
635            Some("2021.2.1f1/def456ghi789"),
636            &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1"],
637            800
638        );
639        
640        let result = Version::from_string_containing(&test_data);
641        assert!(result.is_ok(), "Should extract underscore hash version");
642        
643        let version = result.unwrap();
644        // Should prioritize the underscore version over slash and standalone versions
645        assert_eq!(version.base.major, 2022);
646        assert_eq!(version.base.minor, 3);
647        assert_eq!(version.base.patch, 2);
648        assert_eq!(version.release_type, ReleaseType::Final);
649        assert_eq!(version.revision, 1);
650    }
651
652    #[test]
653    fn prioritizes_slash_hash_when_no_other_hash_formats() {
654        let test_data = generate_test_data_with_versions(
655            None, // No parentheses version
656            None, // No underscore version
657            Some("2021.2.1f1/def456ghi789"),
658            &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1", "2025.1.0b1"],
659            600
660        );
661        
662        let result = Version::from_string_containing(&test_data);
663        assert!(result.is_ok(), "Should extract slash hash version");
664        
665        let version = result.unwrap();
666        // Should prioritize the slash version over standalone versions
667        assert_eq!(version.base.major, 2021);
668        assert_eq!(version.base.minor, 2);
669        assert_eq!(version.base.patch, 1);
670        assert_eq!(version.release_type, ReleaseType::Final);
671        assert_eq!(version.revision, 1);
672    }
673
674    #[test]
675    fn falls_back_to_first_standalone_version_when_no_hash_versions() {
676        let test_data = generate_test_data_with_versions(
677            None, // No parentheses version
678            None, // No underscore version  
679            None, // No slash version
680            &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1", "2025.1.0b1"],
681            400
682        );
683        
684        let result = Version::from_string_containing(&test_data);
685        assert!(result.is_ok(), "Should extract first standalone version");
686        
687        let version = result.unwrap();
688        // Should find the first standalone version when no hash versions exist
689        assert_eq!(version.base.major, 2020);
690        assert_eq!(version.base.minor, 1);
691        assert_eq!(version.base.patch, 0);
692        assert_eq!(version.release_type, ReleaseType::Final);
693        assert_eq!(version.revision, 1);
694    }
695
696    #[test]
697    fn handles_multiple_versions_with_same_priority_returns_first_found() {
698        let test_data = generate_test_data_with_versions(
699            Some("2023.1.5f1 (abc123def456)"),
700            None,
701            None,
702            &["2020.1.0f1"],
703            200
704        );
705        
706        // Add another parentheses version earlier in the data
707        let mut modified_data = String::new();
708        modified_data.push_str("Early data\n");
709        modified_data.push_str("2024.2.1f1 (first123hash)\n");
710        modified_data.push_str("More early data\n");
711        modified_data.push_str(&test_data);
712        
713        let result = Version::from_string_containing(&modified_data);
714        assert!(result.is_ok(), "Should extract first parentheses version");
715        
716        let version = result.unwrap();
717        // Should find the first parentheses version encountered
718        assert_eq!(version.base.major, 2024);
719        assert_eq!(version.base.minor, 2);
720        assert_eq!(version.base.patch, 1);
721        assert_eq!(version.release_type, ReleaseType::Final);
722        assert_eq!(version.revision, 1);
723    }
724
725
726    #[test]  
727    fn handles_extremely_large_dataset_with_performance() {
728        // Generate a very large dataset to test performance
729        let test_data = generate_test_data_with_versions(
730            Some("2023.3.10f1 (abc123def456)"),
731            Some("2022.1.1f1_def456ghi789"),
732            None,
733            &[
734                "2021.1.0f1", "2020.3.15f1", "2019.4.28f1", "2018.4.36f1",
735                "2017.4.40f1", "2016.4.39f1", "2015.4.39f1", "5.6.7f1",
736                "5.5.6f1", "5.4.6f1", "5.3.8f2", "5.2.5f1"
737            ],
738            5000  // Very large bogus dataset
739        );
740        
741        let start = std::time::Instant::now();
742        let result = Version::from_string_containing(&test_data);
743        let duration = start.elapsed();
744        
745        assert!(result.is_ok(), "Should handle large dataset");
746        assert!(duration.as_millis() < 100, "Should parse large dataset quickly (took {:?})", duration);
747        
748        let version = result.unwrap();
749        // Should still prioritize correctly even in large dataset
750        assert_eq!(version.base.major, 2023);
751        assert_eq!(version.base.minor, 3);
752        assert_eq!(version.base.patch, 10);
753        assert_eq!(version.release_type, ReleaseType::Final);
754        assert_eq!(version.revision, 1);
755    }
756
757    #[test]
758    fn prioritizes_versions_with_hashes() {
759        // Test content with multiple versions, including ones with hashes
760        let test_content = r#"
761/lib64/ld-linux-x86-64.so.2
762__libc_start_main
7632020.2.0b2
7642018.1.0b7
7656000.2.0f1 (eed1c594c913)
7666000.2.0f1_eed1c594c913
7672022.2.0a1
7682018.3.0a1
7696000.2/respin/6000.2.0f1-517f89d850d1
7705.0.0a1
7716000.2.0f1.2588.6057
7722017.2.0b1
7736000.2.0f1
774"#;
775
776        let result = Version::from_string_containing(test_content);
777        assert!(result.is_ok(), "Should successfully extract a version");
778
779        let version = result.unwrap();
780        // Should prioritize the version with parentheses hash: 6000.2.0f1 (eed1c594c913)
781        assert_eq!(version.base.major, 6000);
782        assert_eq!(version.base.minor, 2);
783        assert_eq!(version.base.patch, 0);
784        assert_eq!(version.release_type, ReleaseType::Final);
785        assert_eq!(version.revision, 1);
786    }
787
788    #[test]
789    fn handles_fallback_to_versions_without_hashes() {
790        // Test content with only versions without hashes
791        let test_content = r#"
792Some random text
7932020.2.0b2
794More text
7952018.1.0b7
796Even more text
797"#;
798
799        let result = Version::from_string_containing(test_content);
800        assert!(result.is_ok(), "Should successfully extract a version");
801
802        let version = result.unwrap();
803        // Should find the first valid version when no hashed versions exist
804        assert_eq!(version.base.major, 2020);
805        assert_eq!(version.base.minor, 2);
806        assert_eq!(version.base.patch, 0);
807        assert_eq!(version.release_type, ReleaseType::Beta);
808        assert_eq!(version.revision, 2);
809    }
810
811    proptest! {
812        #[test]
813        fn from_str_does_not_crash(s in "\\PC*") {
814            let _v = Version::from_str(&s);
815        }
816
817        #[test]
818        fn from_str_supports_all_valid_cases(
819            major in 0u64..=u64::MAX,
820            minor in 0u64..=u64::MAX,
821            patch in 0u64..=u64::MAX,
822            release_type in prop_oneof!["f", "p", "b", "a"],
823            revision in 0u64..=u64::MAX,
824        ) {
825            let version_string = format!("{}.{}.{}{}{}", major, minor, patch, release_type, revision);
826            let version = Version::from_str(&version_string).unwrap();
827
828            assert!(version.base.major == major, "parse correct major component");
829            assert!(version.base.minor == minor, "parse correct minor component");
830            assert!(version.base.patch == patch, "parse correct patch component");
831
832            assert_eq!(version.release_type, ReleaseType::from_str(&release_type).unwrap());
833            assert!(version.revision == revision, "parse correct revision component");
834        }
835    }
836}