Line data Source code
1 : //!
2 : //! Common utilities for dealing with PostgreSQL relation files.
3 : //!
4 : use once_cell::sync::OnceCell;
5 : use regex::Regex;
6 :
7 : use postgres_ffi_types::forknum::*;
8 :
9 : /// Parse a filename of a relation file. Returns (relfilenode, forknum, segno) tuple.
10 : ///
11 : /// Formats:
12 : ///
13 : /// ```text
14 : /// <oid>
15 : /// <oid>_<fork name>
16 : /// <oid>.<segment number>
17 : /// <oid>_<fork name>.<segment number>
18 : /// ```
19 : ///
20 : /// See functions relpath() and _mdfd_segpath() in PostgreSQL sources.
21 : ///
22 963 : pub fn parse_relfilename(fname: &str) -> Result<(u32, u8, u32), FilePathError> {
23 : static RELFILE_RE: OnceCell<Regex> = OnceCell::new();
24 963 : RELFILE_RE.get_or_init(|| {
25 4 : Regex::new(r"^(?P<relnode>\d+)(_(?P<forkname>[a-z]+))?(\.(?P<segno>\d+))?$").unwrap()
26 4 : });
27 :
28 963 : let caps = RELFILE_RE
29 963 : .get()
30 963 : .unwrap()
31 963 : .captures(fname)
32 963 : .ok_or(FilePathError::InvalidFileName)?;
33 :
34 959 : let relnode_str = caps.name("relnode").unwrap().as_str();
35 959 : let relnode = relnode_str
36 959 : .parse::<u32>()
37 959 : .map_err(|_e| FilePathError::InvalidFileName)?;
38 :
39 958 : let forkname = caps.name("forkname").map(|f| f.as_str());
40 958 : let forknum = forkname_to_number(forkname)?;
41 :
42 957 : let segno_match = caps.name("segno");
43 957 : let segno = if segno_match.is_none() {
44 952 : 0
45 : } else {
46 5 : segno_match
47 5 : .unwrap()
48 5 : .as_str()
49 5 : .parse::<u32>()
50 5 : .map_err(|_e| FilePathError::InvalidFileName)?
51 : };
52 :
53 957 : Ok((relnode, forknum, segno))
54 963 : }
55 :
56 : #[cfg(test)]
57 : mod tests {
58 : use super::*;
59 :
60 : #[test]
61 1 : fn test_parse_valid_relfilenames() {
62 1 : assert_eq!(parse_relfilename("1234"), Ok((1234, 0, 0)));
63 1 : assert_eq!(parse_relfilename("1234_fsm"), Ok((1234, 1, 0)));
64 1 : assert_eq!(parse_relfilename("1234_vm"), Ok((1234, 2, 0)));
65 1 : assert_eq!(parse_relfilename("1234_init"), Ok((1234, 3, 0)));
66 :
67 1 : assert_eq!(parse_relfilename("1234.12"), Ok((1234, 0, 12)));
68 1 : assert_eq!(parse_relfilename("1234_fsm.12"), Ok((1234, 1, 12)));
69 1 : assert_eq!(parse_relfilename("1234_vm.12"), Ok((1234, 2, 12)));
70 1 : assert_eq!(parse_relfilename("1234_init.12"), Ok((1234, 3, 12)));
71 :
72 : // relfilenode is unsigned, so it can go up to 2^32-1
73 1 : assert_eq!(parse_relfilename("3147483648"), Ok((3147483648, 0, 0)));
74 1 : }
75 :
76 : #[test]
77 1 : fn test_parse_invalid_relfilenames() {
78 1 : assert_eq!(
79 1 : parse_relfilename("foo"),
80 : Err(FilePathError::InvalidFileName)
81 : );
82 1 : assert_eq!(
83 1 : parse_relfilename("1.2.3"),
84 : Err(FilePathError::InvalidFileName)
85 : );
86 1 : assert_eq!(
87 1 : parse_relfilename("1234_invalid"),
88 : Err(FilePathError::InvalidForkName)
89 : );
90 1 : assert_eq!(
91 1 : parse_relfilename("1234_"),
92 : Err(FilePathError::InvalidFileName)
93 : );
94 :
95 : // too large for u32
96 1 : assert_eq!(
97 1 : parse_relfilename("12345678901"),
98 : Err(FilePathError::InvalidFileName)
99 : );
100 1 : assert_eq!(
101 1 : parse_relfilename("-1234"),
102 : Err(FilePathError::InvalidFileName)
103 : );
104 1 : }
105 :
106 : #[test]
107 1 : fn test_parse_weird_relfilenames() {
108 : // we accept 0 for the relfilenode, but PostgreSQL should never do that.
109 1 : assert_eq!(parse_relfilename("0"), Ok((0, 0, 0)));
110 :
111 : // PostgreSQL has a limit of 2^32-2 blocks in a table. With 8k block size and
112 : // 1 GB segments, the max segment number is 32767. But we accept larger values
113 : // currently.
114 1 : assert_eq!(parse_relfilename("1.123456"), Ok((1, 0, 123456)));
115 1 : }
116 : }
|