Skip to content

Commit 8df80d9

Browse files
authored
accounts-db: unpack_archive: unpack accounts straight into their final destination (pyth-network#289)
* accounts-db: unpack_archive: avoid extra iteration on each path We used to do a iterator.clone().any(...) followed by iterator.collect(). Merge the two and avoid an extra iteration and re-parsing of the path. * accounts-db: unpack_archive: unpack accounts straight into their final destination We used to unpack accounts into account_path/accounts/<account> then rename to account_path/<account>. We now unpack them into their final destination directly and avoid the rename syscall.
1 parent 228413c commit 8df80d9

File tree

1 file changed

+50
-26
lines changed

1 file changed

+50
-26
lines changed

accounts-db/src/hardened_unpack.rs

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -112,27 +112,26 @@ where
112112
// first by ourselves when there are odd paths like including `..` or /
113113
// for our clearer pattern matching reasoning:
114114
// https://docs.rs/tar/0.4.26/src/tar/entry.rs.html#371
115-
let parts = path.components().map(|p| match p {
116-
CurDir => Some("."),
117-
Normal(c) => c.to_str(),
118-
_ => None, // Prefix (for Windows) and RootDir are forbidden
119-
});
115+
let parts = path
116+
.components()
117+
.map(|p| match p {
118+
CurDir => Ok("."),
119+
Normal(c) => c.to_str().ok_or(()),
120+
_ => Err(()), // Prefix (for Windows) and RootDir are forbidden
121+
})
122+
.collect::<std::result::Result<Vec<_>, _>>();
120123

121124
// Reject old-style BSD directory entries that aren't explicitly tagged as directories
122125
let legacy_dir_entry =
123126
entry.header().as_ustar().is_none() && entry.path_bytes().ends_with(b"/");
124127
let kind = entry.header().entry_type();
125128
let reject_legacy_dir_entry = legacy_dir_entry && (kind != Directory);
126-
127-
if parts.clone().any(|p| p.is_none()) || reject_legacy_dir_entry {
129+
let (Ok(parts), false) = (parts, reject_legacy_dir_entry) else {
128130
return Err(UnpackError::Archive(format!(
129131
"invalid path found: {path_str:?}"
130132
)));
131-
}
133+
};
132134

133-
let parts: Vec<_> = parts.map(|p| p.unwrap()).collect();
134-
let account_filename =
135-
(parts.len() == 2 && parts[0] == "accounts").then(|| PathBuf::from(parts[1]));
136135
let unpack_dir = match entry_checker(parts.as_slice(), kind) {
137136
UnpackPath::Invalid => {
138137
return Err(UnpackError::Archive(format!(
@@ -159,30 +158,32 @@ where
159158
)?;
160159
total_count = checked_total_count_increment(total_count, limit_count)?;
161160

162-
let target = sanitize_path(&entry.path()?, unpack_dir)?; // ? handles file system errors
163-
if target.is_none() {
161+
let account_filename = match parts.as_slice() {
162+
["accounts", account_filename] => Some(PathBuf::from(account_filename)),
163+
_ => None,
164+
};
165+
let entry_path = if let Some(account) = account_filename {
166+
// Special case account files. We're unpacking an account entry inside one of the
167+
// account_paths returned by `entry_checker`. We want to unpack into
168+
// account_path/<account> instead of account_path/accounts/<account> so we strip the
169+
// accounts/ prefix.
170+
sanitize_path(&account, unpack_dir)
171+
} else {
172+
sanitize_path(&path, unpack_dir)
173+
}?; // ? handles file system errors
174+
let Some(entry_path) = entry_path else {
164175
continue; // skip it
165-
}
166-
let target = target.unwrap();
176+
};
167177

168-
let unpack = entry.unpack(target);
178+
let unpack = entry.unpack(&entry_path);
169179
check_unpack_result(unpack.map(|_unpack| true)?, path_str)?;
170180

171181
// Sanitize permissions.
172182
let mode = match entry.header().entry_type() {
173183
GNUSparse | Regular => 0o644,
174184
_ => 0o755,
175185
};
176-
let entry_path_buf = unpack_dir.join(entry.path()?);
177-
set_perms(&entry_path_buf, mode)?;
178-
179-
let entry_path = if let Some(account_filename) = account_filename {
180-
let stripped_path = unpack_dir.join(account_filename); // strip away "accounts"
181-
fs::rename(&entry_path_buf, &stripped_path)?;
182-
stripped_path
183-
} else {
184-
entry_path_buf
185-
};
186+
set_perms(&entry_path, mode)?;
186187

187188
// Process entry after setting permissions
188189
entry_processor(entry_path);
@@ -1029,4 +1030,27 @@ mod tests {
10291030
if message == "too many files in snapshot: 1000000000000"
10301031
);
10311032
}
1033+
1034+
#[test]
1035+
fn test_archive_unpack_account_path() {
1036+
let mut header = Header::new_gnu();
1037+
header.set_path("accounts/123.456").unwrap();
1038+
header.set_size(4);
1039+
header.set_cksum();
1040+
let data: &[u8] = &[1, 2, 3, 4];
1041+
1042+
let mut archive = Builder::new(Vec::new());
1043+
archive.append(&header, data).unwrap();
1044+
let result = with_finalize_and_unpack(archive, |ar, tmp| {
1045+
unpack_snapshot_with_processors(
1046+
ar,
1047+
tmp,
1048+
&[tmp.join("accounts_dest")],
1049+
None,
1050+
|_, _| {},
1051+
|path| assert_eq!(path, tmp.join("accounts_dest/123.456")),
1052+
)
1053+
});
1054+
assert_matches!(result, Ok(()));
1055+
}
10321056
}

0 commit comments

Comments
 (0)