affs_read/
symlink.rs

1//! Symlink reading functionality.
2
3use crate::constants::*;
4
5/// Maximum symlink target length.
6///
7/// For a 512-byte block, symlink data starts at offset 24 and ends before
8/// the file header structure at offset 312 (512 - 200), giving 288 bytes.
9/// For larger block sizes, this grows proportionally.
10pub const MAX_SYMLINK_LEN: usize = BLOCK_SIZE - SYMLINK_OFFSET - FILE_LOCATION;
11
12/// Read symlink target from a block buffer.
13///
14/// The symlink target is stored as a Latin1 string starting at offset 24
15/// (GRUB_AFFS_SYMLINK_OFFSET) in the entry block.
16///
17/// # Arguments
18/// * `buf` - The entry block data (512 bytes for standard block size)
19/// * `out` - Output buffer for UTF-8 converted target
20///
21/// # Returns
22/// The number of bytes written to `out`, or an error.
23///
24/// # Notes
25/// - The target is null-terminated in the block
26/// - Latin1 characters are converted to UTF-8
27/// - Leading `:` is replaced with `/` (Amiga volume reference)
28pub fn read_symlink_target(buf: &[u8; BLOCK_SIZE], out: &mut [u8]) -> usize {
29    read_symlink_target_with_block_size(buf, BLOCK_SIZE, out)
30}
31
32/// Read symlink target with variable block size support.
33///
34/// # Arguments
35/// * `buf` - The entry block data
36/// * `block_size` - The filesystem block size
37/// * `out` - Output buffer for UTF-8 converted target
38///
39/// # Returns
40/// The number of bytes written to `out`.
41pub fn read_symlink_target_with_block_size(buf: &[u8], block_size: usize, out: &mut [u8]) -> usize {
42    // Calculate symlink data region
43    let symlink_start = SYMLINK_OFFSET;
44    let symlink_end = block_size.saturating_sub(FILE_LOCATION);
45
46    if symlink_start >= symlink_end || symlink_start >= buf.len() {
47        return 0;
48    }
49
50    let symlink_end = symlink_end.min(buf.len());
51    let latin1 = &buf[symlink_start..symlink_end];
52
53    let len = memchr::memchr(0, latin1).unwrap_or(latin1.len());
54    let latin1 = &latin1[..len];
55
56    // Convert Latin1 to UTF-8 with `:` -> `/` replacement
57    latin1_to_utf8_symlink(latin1, out)
58}
59
60/// Convert Latin1 bytes to UTF-8, replacing leading `:` with `/`.
61///
62/// In Amiga paths, `:` refers to the volume root. GRUB replaces this
63/// with `/` for Unix compatibility.
64///
65/// # Arguments
66/// * `latin1` - Input Latin1 bytes
67/// * `out` - Output buffer for UTF-8
68///
69/// # Returns
70/// Number of bytes written to `out`.
71fn latin1_to_utf8_symlink(latin1: &[u8], out: &mut [u8]) -> usize {
72    let mut out_pos = 0;
73
74    for (i, &byte) in latin1.iter().enumerate() {
75        // Replace leading `:` with `/`
76        let byte = if i == 0 && byte == b':' { b'/' } else { byte };
77
78        if byte < 0x80 {
79            // ASCII - direct copy
80            if out_pos >= out.len() {
81                break;
82            }
83            out[out_pos] = byte;
84            out_pos += 1;
85        } else {
86            // Latin1 high byte (0x80-0xFF) -> UTF-8 two-byte sequence
87            // UTF-8: 110xxxxx 10xxxxxx
88            if out_pos + 1 >= out.len() {
89                break;
90            }
91            out[out_pos] = 0xC0 | (byte >> 6);
92            out[out_pos + 1] = 0x80 | (byte & 0x3F);
93            out_pos += 2;
94        }
95    }
96
97    out_pos
98}
99
100/// Calculate maximum UTF-8 length for a Latin1 string.
101///
102/// Each Latin1 byte can expand to at most 2 UTF-8 bytes.
103#[inline]
104pub const fn max_utf8_len(latin1_len: usize) -> usize {
105    latin1_len * 2
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_latin1_to_utf8_ascii() {
114        let input = b"hello";
115        let mut out = [0u8; 32];
116        let len = latin1_to_utf8_symlink(input, &mut out);
117        assert_eq!(len, 5);
118        assert_eq!(&out[..len], b"hello");
119    }
120
121    #[test]
122    fn test_latin1_to_utf8_high_bytes() {
123        // Latin1: 0xE9 = 'e' with accent (e-acute)
124        // UTF-8: 0xC3 0xA9
125        let input = [0xE9];
126        let mut out = [0u8; 32];
127        let len = latin1_to_utf8_symlink(&input, &mut out);
128        assert_eq!(len, 2);
129        assert_eq!(&out[..len], &[0xC3, 0xA9]);
130    }
131
132    #[test]
133    fn test_colon_replacement() {
134        let input = b":path/to/file";
135        let mut out = [0u8; 32];
136        let len = latin1_to_utf8_symlink(input, &mut out);
137        assert_eq!(len, 13);
138        assert_eq!(&out[..len], b"/path/to/file");
139    }
140
141    #[test]
142    fn test_colon_not_at_start() {
143        let input = b"path:to/file";
144        let mut out = [0u8; 32];
145        let len = latin1_to_utf8_symlink(input, &mut out);
146        assert_eq!(len, 12);
147        assert_eq!(&out[..len], b"path:to/file");
148    }
149
150    #[test]
151    fn test_read_symlink_target() {
152        let mut buf = [0u8; BLOCK_SIZE];
153        // Put a symlink target at offset 24
154        buf[SYMLINK_OFFSET..SYMLINK_OFFSET + 5].copy_from_slice(b"test\0");
155
156        let mut out = [0u8; 32];
157        let len = read_symlink_target(&buf, &mut out);
158        assert_eq!(len, 4);
159        assert_eq!(&out[..len], b"test");
160    }
161
162    #[test]
163    fn test_read_symlink_with_colon() {
164        let mut buf = [0u8; BLOCK_SIZE];
165        buf[SYMLINK_OFFSET..SYMLINK_OFFSET + 6].copy_from_slice(b":boot\0");
166
167        let mut out = [0u8; 32];
168        let len = read_symlink_target(&buf, &mut out);
169        assert_eq!(len, 5);
170        assert_eq!(&out[..len], b"/boot");
171    }
172}