aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-src/src/standard/versioning.rs
blob: 8dadaafc05b58f16dcc5f81b4ebbc82a00fd04bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/// php-src: ext/standard/versioning.c `php_version_compare` (PHP 8.5.2)
///
/// Returns -1, 0 or 1. The original walks the canonicalized strings with destructive `.` splits
/// and moving pointers; this walks a `split('.')` iterator over each instead.
pub fn php_version_compare(v1: &str, v2: &str) -> i32 {
    if v1.is_empty() || v2.is_empty() {
        return match (v1.is_empty(), v2.is_empty()) {
            (true, true) => 0,
            (false, _) => 1,
            (_, false) => -1,
        };
    }
    let c1 = canonicalize_version(v1);
    let c2 = canonicalize_version(v2);
    let mut t1 = c1.split('.').filter(|s| !s.is_empty());
    let mut t2 = c2.split('.').filter(|s| !s.is_empty());

    let mut compare = 0;
    let mut p1 = t1.next();
    let mut p2 = t2.next();
    while compare == 0
        && let (Some(a), Some(b)) = (p1, p2)
    {
        compare = version_token_compare(a, b);
        p1 = t1.next();
        p2 = t2.next();
    }
    if compare == 0 {
        // A leftover numeric token wins; a leftover special form is compared against the implicit
        // release baseline ("#", order 4).
        if let Some(p) = p1 {
            compare = if p.as_bytes()[0].is_ascii_digit() {
                1
            } else {
                special_form_order(p).cmp(&4) as i32
            };
        } else if let Some(p) = p2 {
            compare = if p.as_bytes()[0].is_ascii_digit() {
                -1
            } else {
                4.cmp(&special_form_order(p)) as i32
            };
        }
    }
    compare
}

/// php-src: ext/standard/versioning.c `php_canonicalize_version` (PHP 8.5.2)
///
/// Separators (-, _, +, .) collapse to a single '.', and a '.' is inserted at every digit <->
/// non-digit boundary. The original's `!isalnum(*p)` branch is not ported.
fn canonicalize_version(version: &str) -> String {
    let bytes = version.as_bytes();
    if bytes.is_empty() {
        return String::new();
    }
    let mut q: Vec<u8> = Vec::with_capacity(bytes.len() * 2);
    q.push(bytes[0]);
    for &raw in &bytes[1..] {
        let ch = if matches!(raw, b'-' | b'_' | b'+') {
            b'.'
        } else {
            raw
        };
        let last = *q.last().unwrap();
        if ch == b'.' {
            if last != b'.' {
                q.push(b'.');
            }
        } else if last.is_ascii_digit() != ch.is_ascii_digit() {
            q.push(b'.');
            q.push(ch);
        } else {
            q.push(ch);
        }
    }
    // Splits are only ever inserted at ASCII digit <-> non-digit boundaries, which are always
    // char boundaries, so a valid UTF-8 input stays valid.
    String::from_utf8(q).expect("canonicalized version is valid UTF-8")
}

/// php-src: ext/standard/versioning.c `php_version_compare` loop body (PHP 8.5.2)
fn version_token_compare(t1: &str, t2: &str) -> i32 {
    let d1 = t1.as_bytes()[0].is_ascii_digit();
    let d2 = t2.as_bytes()[0].is_ascii_digit();
    if d1 && d2 {
        let l1 = t1.parse::<i64>().unwrap_or(0);
        let l2 = t2.parse::<i64>().unwrap_or(0);
        l1.cmp(&l2) as i32
    } else if !d1 && !d2 {
        special_form_order(t1).cmp(&special_form_order(t2)) as i32
    } else if d1 {
        // A numeric token is treated as the "#" form (order 4).
        4.cmp(&special_form_order(t2)) as i32
    } else {
        special_form_order(t1).cmp(&4) as i32
    }
}

/// php-src: ext/standard/versioning.c `compare_special_version_forms` (PHP 8.5.2)
fn special_form_order(form: &str) -> i32 {
    const FORMS: &[(&str, i32)] = &[
        ("dev", 0),
        ("alpha", 1),
        ("a", 1),
        ("beta", 2),
        ("b", 2),
        ("RC", 3),
        ("rc", 3),
        ("#", 4),
        ("pl", 5),
        ("p", 5),
    ];
    for (name, order) in FORMS {
        if form.starts_with(name) {
            return *order;
        }
    }
    -1
}