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