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)
30        .map_err(|source| Error::io(format!("opening zip {}", zip_path.display()), source))?;
31    let mut archive = zip::ZipArchive::new(file).map_err(Error::Zip)?;
32
33    fs::create_dir_all(dest_dir)
34        .map_err(|source| Error::io(format!("creating {}", dest_dir.display()), source))?;
35
36    let mut extracted = Vec::new();
37    let mut found_hyperd = false;
38
39    for i in 0..archive.len() {
40        let mut entry = archive.by_index(i).map_err(Error::Zip)?;
41        let Some(enclosed) = entry.enclosed_name() else {
42            continue;
43        };
44        let Some(rel) = strip_lib_hyper_prefix(&enclosed) else {
45            continue;
46        };
47        if rel.as_os_str().is_empty() {
48            continue;
49        }
50
51        let out_path = dest_dir.join(&rel);
52        if entry.is_dir() {
53            fs::create_dir_all(&out_path)
54                .map_err(|source| Error::io(format!("creating {}", out_path.display()), source))?;
55            continue;
56        }
57        if let Some(parent) = out_path.parent() {
58            fs::create_dir_all(parent)
59                .map_err(|source| Error::io(format!("creating {}", parent.display()), source))?;
60        }
61        let mut out = File::create(&out_path)
62            .map_err(|source| Error::io(format!("creating {}", out_path.display()), source))?;
63        io::copy(&mut entry, &mut out)
64            .map_err(|source| Error::io(format!("writing {}", out_path.display()), source))?;
65
66        #[cfg(unix)]
67        {
68            use std::os::unix::fs::PermissionsExt;
69            if let Some(mode) = entry.unix_mode() {
70                let _ = fs::set_permissions(&out_path, fs::Permissions::from_mode(mode));
71            }
72        }
73
74        if rel
75            .file_name()
76            .is_some_and(|n| n == "hyperd" || n == "hyperd.exe")
77        {
78            found_hyperd = true;
79        }
80        extracted.push(rel);
81    }
82
83    if !found_hyperd {
84        return Err(Error::HyperdNotInArchive);
85    }
86    Ok(extracted)
87}
88
89/// Return the path stripped of a leading `lib/hyper/` or `bin/hyper/` prefix,
90/// or `None` if the entry is outside those directories. The Hyper C++ API zip
91/// wraps everything in a top-level `tableauhyperapi-cxx-...` directory and
92/// nests the runtime under `lib/hyper/` (Linux/macOS) or `bin/hyper/`
93/// (Windows) inside it.
94fn strip_lib_hyper_prefix(path: &Path) -> Option<PathBuf> {
95    let mut comps = path.components();
96    // Skip one optional top-level wrapper component (e.g.
97    // `tableauhyperapi-cxx-macos-arm64-release-main.0.0.24457.rc36858b6`)
98    // before looking for the `lib/hyper` or `bin/hyper` pair.
99    let first = comps.next()?;
100    let (a, b) = if first.as_os_str() == "lib" || first.as_os_str() == "bin" {
101        (first, comps.next()?)
102    } else {
103        (comps.next()?, comps.next()?)
104    };
105    if (a.as_os_str() == "lib" || a.as_os_str() == "bin") && b.as_os_str() == "hyper" {
106        Some(comps.as_path().to_path_buf())
107    } else {
108        None
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn strip_prefix_matches_lib_hyper() {
118        // Bare form (no wrapping top-level dir).
119        assert_eq!(
120            strip_lib_hyper_prefix(Path::new("lib/hyper/hyperd")),
121            Some(PathBuf::from("hyperd"))
122        );
123        assert_eq!(
124            strip_lib_hyper_prefix(Path::new("lib/hyper/sub/file")),
125            Some(PathBuf::from("sub/file"))
126        );
127        // Real-world form: wrapped under a versioned top-level dir (Linux/macOS).
128        assert_eq!(
129            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/lib/hyper/hyperd")),
130            Some(PathBuf::from("hyperd"))
131        );
132        assert_eq!(
133            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/lib/hyper/sub/a.so")),
134            Some(PathBuf::from("sub/a.so"))
135        );
136        // Windows uses bin/hyper/ instead of lib/hyper/.
137        assert_eq!(
138            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/hyper/hyperd.exe")),
139            Some(PathBuf::from("hyperd.exe"))
140        );
141        assert_eq!(
142            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/hyper/sub/extra.dll")),
143            Some(PathBuf::from("sub/extra.dll"))
144        );
145        // Anything outside lib/hyper or bin/hyper is dropped.
146        assert_eq!(
147            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/include/foo.hpp")),
148            None
149        );
150        assert_eq!(
151            strip_lib_hyper_prefix(Path::new("tableauhyperapi-cxx-x/bin/tableauhyperapi.dll")),
152            None
153        );
154        assert_eq!(strip_lib_hyper_prefix(Path::new("other/file")), None);
155    }
156
157    #[test]
158    fn extract_fixture_zip() -> Result<(), Box<dyn std::error::Error>> {
159        use std::io::Write;
160        let tmp = tempfile::tempdir()?;
161        let zip_path = tmp.path().join("fixture.zip");
162        {
163            let file = File::create(&zip_path)?;
164            let mut zw = zip::ZipWriter::new(file);
165            let opts = zip::write::SimpleFileOptions::default();
166            // Mirror the real release zip's top-level wrapper dir.
167            zw.start_file("tableauhyperapi-cxx-fake/lib/hyper/hyperd", opts)?;
168            zw.write_all(b"fake hyperd")?;
169            zw.start_file("tableauhyperapi-cxx-fake/lib/hyper/sub/extra.txt", opts)?;
170            zw.write_all(b"extra")?;
171            zw.start_file("tableauhyperapi-cxx-fake/include/ignored.hpp", opts)?;
172            zw.write_all(b"nope")?;
173            zw.finish()?;
174        }
175        let out = tmp.path().join("out");
176        let files = extract_hyperd(&zip_path, &out)?;
177        assert!(files.iter().any(|p| p == Path::new("hyperd")));
178        assert!(files.iter().any(|p| p == Path::new("sub/extra.txt")));
179        assert!(out.join("hyperd").exists());
180        assert!(!out.join("ignored.hpp").exists());
181        Ok(())
182    }
183}