Skip to main content

hyperdb_bootstrap/
extract.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! ZIP-archive extraction for the Hyper C++ API release bundle.
5//!
6//! The upstream archive nests `hyperd` plus its shared libraries inside a
7//! versioned top-level directory (e.g.
8//! `tableauhyperapi-cxx-macos-arm64-release-main.0.0.24457.rc36858b6/`) and
9//! then under `lib/hyper/` on Linux/macOS or `bin/hyper/` on Windows. This
10//! module flattens both layers so downstream consumers only see the
11//! `hyperd` runtime files.
12
13use std::fs::{self, File};
14use std::io;
15use std::path::{Path, PathBuf};
16
17use crate::Error;
18
19/// Extract everything under `lib/hyper/` (or `bin/hyper/` on Windows) from
20/// the Hyper C++ API zip into `dest_dir`, flattening the wrapper prefixes
21/// away. Returns the list of extracted file paths relative to `dest_dir`.
22///
23/// # Errors
24///
25/// Returns [`Error::Io`] for filesystem failures, [`Error::Zip`] if the
26/// archive cannot be opened or parsed, and [`Error::HyperdNotInArchive`]
27/// if the archive does not contain a `hyperd` / `hyperd.exe` entry.
28pub fn extract_hyperd(zip_path: &Path, dest_dir: &Path) -> Result<Vec<PathBuf>, Error> {
29    let file = File::open(zip_path).map_err(|source| Error::Io {
30        context: format!("opening zip {}", zip_path.display()),
31        source,
32    })?;
33    let mut archive = zip::ZipArchive::new(file).map_err(Error::Zip)?;
34
35    fs::create_dir_all(dest_dir).map_err(|source| Error::Io {
36        context: format!("creating {}", dest_dir.display()),
37        source,
38    })?;
39
40    let mut extracted = Vec::new();
41    let mut found_hyperd = false;
42
43    for i in 0..archive.len() {
44        let mut entry = archive.by_index(i).map_err(Error::Zip)?;
45        let Some(enclosed) = entry.enclosed_name() else {
46            continue;
47        };
48        let Some(rel) = strip_lib_hyper_prefix(&enclosed) else {
49            continue;
50        };
51        if rel.as_os_str().is_empty() {
52            continue;
53        }
54
55        let out_path = dest_dir.join(&rel);
56        if entry.is_dir() {
57            fs::create_dir_all(&out_path).map_err(|source| Error::Io {
58                context: format!("creating {}", out_path.display()),
59                source,
60            })?;
61            continue;
62        }
63        if let Some(parent) = out_path.parent() {
64            fs::create_dir_all(parent).map_err(|source| Error::Io {
65                context: format!("creating {}", parent.display()),
66                source,
67            })?;
68        }
69        let mut out = File::create(&out_path).map_err(|source| Error::Io {
70            context: format!("creating {}", out_path.display()),
71            source,
72        })?;
73        io::copy(&mut entry, &mut out).map_err(|source| Error::Io {
74            context: format!("writing {}", out_path.display()),
75            source,
76        })?;
77
78        #[cfg(unix)]
79        {
80            use std::os::unix::fs::PermissionsExt;
81            if let Some(mode) = entry.unix_mode() {
82                let _ = fs::set_permissions(&out_path, fs::Permissions::from_mode(mode));
83            }
84        }
85
86        if rel
87            .file_name()
88            .is_some_and(|n| n == "hyperd" || n == "hyperd.exe")
89        {
90            found_hyperd = true;
91        }
92        extracted.push(rel);
93    }
94
95    if !found_hyperd {
96        return Err(Error::HyperdNotInArchive);
97    }
98    Ok(extracted)
99}
100
101/// Return the path stripped of a leading `lib/hyper/` or `bin/hyper/` prefix,
102/// or `None` if the entry is outside those directories. The Hyper C++ API zip
103/// wraps everything in a top-level `tableauhyperapi-cxx-...` directory and
104/// nests the runtime under `lib/hyper/` (Linux/macOS) or `bin/hyper/`
105/// (Windows) inside it.
106fn strip_lib_hyper_prefix(path: &Path) -> Option<PathBuf> {
107    let mut comps = path.components();
108    // Skip one optional top-level wrapper component (e.g.
109    // `tableauhyperapi-cxx-macos-arm64-release-main.0.0.24457.rc36858b6`)
110    // before looking for the `lib/hyper` or `bin/hyper` pair.
111    let first = comps.next()?;
112    let (a, b) = if first.as_os_str() == "lib" || first.as_os_str() == "bin" {
113        (first, comps.next()?)
114    } else {
115        (comps.next()?, comps.next()?)
116    };
117    if (a.as_os_str() == "lib" || a.as_os_str() == "bin") && b.as_os_str() == "hyper" {
118        Some(comps.as_path().to_path_buf())
119    } else {
120        None
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn strip_prefix_matches_lib_hyper() {
130        // Bare form (no wrapping top-level dir).
131        assert_eq!(
132            strip_lib_hyper_prefix(Path::new("lib/hyper/hyperd")),
133            Some(PathBuf::from("hyperd"))
134        );
135        assert_eq!(
136            strip_lib_hyper_prefix(Path::new("lib/hyper/sub/file")),
137            Some(PathBuf::from("sub/file"))
138        );
139        // Real-world form: wrapped under a versioned top-level dir (Linux/macOS).
140        assert_eq!(
141            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/lib/hyper/hyperd")),
142            Some(PathBuf::from("hyperd"))
143        );
144        assert_eq!(
145            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/lib/hyper/sub/a.so")),
146            Some(PathBuf::from("sub/a.so"))
147        );
148        // Windows uses bin/hyper/ instead of lib/hyper/.
149        assert_eq!(
150            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/hyper/hyperd.exe")),
151            Some(PathBuf::from("hyperd.exe"))
152        );
153        assert_eq!(
154            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/hyper/sub/extra.dll")),
155            Some(PathBuf::from("sub/extra.dll"))
156        );
157        // Anything outside lib/hyper or bin/hyper is dropped.
158        assert_eq!(
159            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/include/foo.hpp")),
160            None
161        );
162        assert_eq!(
163            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/tableauhyperapi.dll")),
164            None
165        );
166        assert_eq!(strip_lib_hyper_prefix(Path::new("other/file")), None);
167    }
168
169    #[test]
170    fn extract_fixture_zip() -> Result<(), Box<dyn std::error::Error>> {
171        use std::io::Write;
172        let tmp = tempfile::tempdir()?;
173        let zip_path = tmp.path().join("fixture.zip");
174        {
175            let file = File::create(&zip_path)?;
176            let mut zw = zip::ZipWriter::new(file);
177            let opts = zip::write::SimpleFileOptions::default();
178            // Mirror the real release zip's top-level wrapper dir.
179            zw.start_file("tableauhyperapi-cxx-fake/lib/hyper/hyperd", opts)?;
180            zw.write_all(b"fake hyperd")?;
181            zw.start_file("tableauhyperapi-cxx-fake/lib/hyper/sub/extra.txt", opts)?;
182            zw.write_all(b"extra")?;
183            zw.start_file("tableauhyperapi-cxx-fake/include/ignored.hpp", opts)?;
184            zw.write_all(b"nope")?;
185            zw.finish()?;
186        }
187        let out = tmp.path().join("out");
188        let files = extract_hyperd(&zip_path, &out)?;
189        assert!(files.iter().any(|p| p == Path::new("hyperd")));
190        assert!(files.iter().any(|p| p == Path::new("sub/extra.txt")));
191        assert!(out.join("hyperd").exists());
192        assert!(!out.join("ignored.hpp").exists());
193        Ok(())
194    }
195}