cargo_lambda_build/
archive.rs

1use std::{
2    collections::HashMap,
3    fmt::Debug,
4    fs::{File, Metadata, read},
5    io::{Read, Seek, Write},
6    path::{Path, PathBuf},
7    time::{Duration, SystemTime},
8};
9
10use cargo_lambda_metadata::{
11    cargo::{CargoMetadata, target_dir_from_metadata},
12    fs::copy_and_replace,
13};
14use cargo_lambda_remote::aws_sdk_lambda::types::Architecture as CpuArchitecture;
15use chrono::{DateTime, NaiveDateTime, Utc};
16use chrono_humanize::HumanTime;
17use miette::{Context, IntoDiagnostic, Result};
18use object::{Architecture, Object, read::File as ObjectFile};
19use serde::{Serialize, Serializer};
20use sha2::{Digest, Sha256};
21use tracing::{debug, trace};
22use walkdir::WalkDir;
23use zip::{HasZipMetadata, ZipArchive, ZipWriter, write::SimpleFileOptions};
24
25use crate::error::BuildError;
26
27#[derive(Clone, Debug)]
28pub struct BinaryModifiedAt(Option<SystemTime>);
29
30impl BinaryModifiedAt {
31    pub fn now() -> Self {
32        Self(Some(SystemTime::now()))
33    }
34}
35
36impl From<NaiveDateTime> for BinaryModifiedAt {
37    fn from(value: NaiveDateTime) -> Self {
38        let dt = DateTime::<Utc>::from_naive_utc_and_offset(value, Utc);
39        let Some(timestamp) = dt.timestamp_nanos_opt() else {
40            return Self(None);
41        };
42
43        let system_time = SystemTime::UNIX_EPOCH + Duration::from_nanos(timestamp as u64);
44        Self(Some(system_time))
45    }
46}
47
48#[derive(Debug)]
49pub enum BinaryData<'a> {
50    Function(&'a str),
51    ExternalExtension(&'a str),
52    InternalExtension(&'a str),
53}
54
55impl<'a> BinaryData<'a> {
56    /// Create a BinaryData given the arguments of the CLI
57    pub fn new(name: &'a str, extension: bool, internal: bool) -> Self {
58        if extension {
59            if internal {
60                BinaryData::InternalExtension(name)
61            } else {
62                BinaryData::ExternalExtension(name)
63            }
64        } else {
65            BinaryData::Function(name)
66        }
67    }
68
69    /// Name of the binary to copy inside the zip archive
70    pub fn binary_name(&self) -> &str {
71        match self {
72            BinaryData::Function(_) => "bootstrap",
73            BinaryData::ExternalExtension(name) | BinaryData::InternalExtension(name) => name,
74        }
75    }
76
77    /// Name of the zip archive
78    pub fn zip_name(&self) -> String {
79        format!("{}.zip", self.binary_name())
80    }
81
82    /// Location of the binary after building it
83    pub fn binary_location(&self) -> &str {
84        match self {
85            BinaryData::Function(name) => name,
86            BinaryData::ExternalExtension(_) | BinaryData::InternalExtension(_) => "extensions",
87        }
88    }
89
90    /// Name of the parent directory to copy the binary into
91    pub fn parent_dir(&self) -> Option<&str> {
92        match self {
93            BinaryData::ExternalExtension(_) => Some("extensions"),
94            _ => None,
95        }
96    }
97
98    /// Command to use to build each kind of binary
99    pub fn build_help(&self) -> &str {
100        match self {
101            BinaryData::Function(_) => "build",
102            BinaryData::ExternalExtension(_) => "build --extension",
103            BinaryData::InternalExtension(_) => "build --extension --internal",
104        }
105    }
106
107    pub(crate) fn binary_path_in_zip(&self) -> Result<String, BuildError> {
108        let file_name = if let Some(parent) = self.parent_dir() {
109            Path::new(parent).join(self.binary_name())
110        } else {
111            PathBuf::from(self.binary_name())
112        };
113
114        convert_to_unix_path(&file_name)
115            .ok_or_else(|| BuildError::InvalidUnixFileName(file_name.clone()))
116    }
117}
118
119pub struct BinaryArchive {
120    pub architecture: String,
121    pub path: PathBuf,
122    pub binary_modified_at: BinaryModifiedAt,
123}
124
125impl BinaryArchive {
126    pub fn new(path: PathBuf, architecture: String, binary_modified_at: BinaryModifiedAt) -> Self {
127        Self {
128            path,
129            architecture,
130            binary_modified_at,
131        }
132    }
133
134    /// Read the content of the binary archive to the end
135    pub fn read(&self) -> Result<Vec<u8>> {
136        read(&self.path)
137            .into_diagnostic()
138            .wrap_err("failed to read binary archive")
139    }
140
141    /// Calculate the SHA256 hash of the zip binary file
142    pub fn sha256(&self) -> Result<String> {
143        let data = self.read()?;
144        let mut hasher = Sha256::new();
145        hasher.update(data);
146        let sha256 = format!("{:X}", hasher.finalize());
147        Ok(sha256)
148    }
149
150    /// List the files inside the zip archive
151    pub fn list(&self) -> Result<Vec<String>> {
152        let zipfile = File::open(&self.path).into_diagnostic()?;
153        let mut archive = ZipArchive::new(zipfile).into_diagnostic()?;
154
155        let mut files = Vec::new();
156        for i in 0..archive.len() {
157            let entry = archive.by_index(i).into_diagnostic()?;
158            files.push(entry.name().to_string());
159        }
160
161        Ok(files)
162    }
163
164    /// Get the architecture of the binary archive
165    pub fn architecture(&self) -> CpuArchitecture {
166        CpuArchitecture::from(self.architecture.as_str())
167    }
168}
169
170/// Search for the binary file for a function or extension inside the target directory.
171/// If the binary file exists, it creates the zip archive and extracts its architecture by reading the binary.
172/// If the zip file already exists, use it as the deployment archive.
173/// If none of them exist, return an error.
174pub fn create_binary_archive<P>(
175    metadata: Option<&CargoMetadata>,
176    base_dir: &Option<P>,
177    data: &BinaryData,
178    include: Option<Vec<String>>,
179) -> Result<BinaryArchive>
180where
181    P: AsRef<Path>,
182{
183    let bootstrap_dir = if let Some(dir) = base_dir {
184        dir.as_ref().join(data.binary_location())
185    } else {
186        let target_dir = metadata
187            .and_then(|m| target_dir_from_metadata(m).ok())
188            .unwrap_or_else(|| PathBuf::from("target"));
189
190        target_dir.join("lambda").join(data.binary_location())
191    };
192
193    let binary_path = bootstrap_dir.join(data.binary_name());
194    if binary_path.exists() {
195        return zip_binary(binary_path, bootstrap_dir, data, include);
196    } else {
197        let zip_path = bootstrap_dir.join(data.zip_name());
198
199        if zip_path.exists() {
200            return use_zip_in_place(zip_path, data, include);
201        }
202    }
203
204    Err(BuildError::BinaryMissing(data.binary_name().into(), data.build_help().into()).into())
205}
206
207fn use_zip_in_place(
208    zip_path: PathBuf,
209    data: &BinaryData<'_>,
210    include: Option<Vec<String>>,
211) -> Result<BinaryArchive> {
212    let binary_path_in_zip = data.binary_path_in_zip()?;
213    let (arch, binary_modified_at) =
214        extract_data_from_zipped_binary(&zip_path, &binary_path_in_zip)?;
215
216    if let Some(files) = include {
217        let file = File::open(&zip_path)
218            .into_diagnostic()
219            .wrap_err_with(|| format!("failed to open zip file `{zip_path:?}`"))?;
220
221        // Zip2 doesn't support updating zip files in place, so we need to create a new zip file
222        // if we want to include files in the zip archive.
223        // Before we do that, we need to read the existing zip file and copy its contents to a new zip file.
224        let mut archive = ZipArchive::new(file).into_diagnostic()?;
225
226        let tmp_dir = tempfile::tempdir().into_diagnostic()?;
227        let tmp_path = tmp_dir.path().join(data.zip_name());
228        let tmp = File::create(&tmp_path).into_diagnostic()?;
229        let mut zip = ZipWriter::new(tmp);
230
231        for i in 0..archive.len() {
232            let file = archive.by_index_raw(i).into_diagnostic()?;
233            zip.raw_copy_file(file).into_diagnostic()?;
234        }
235
236        include_files_in_zip(&mut zip, &files)?;
237
238        zip.finish()
239            .into_diagnostic()
240            .wrap_err_with(|| format!("failed to finish zip file `{}`", tmp_path.display()))?;
241
242        drop(archive);
243        copy_and_replace(&tmp_path, &zip_path).into_diagnostic()?;
244    }
245
246    Ok(BinaryArchive::new(
247        zip_path.clone(),
248        arch.to_string(),
249        binary_modified_at,
250    ))
251}
252
253/// Create a zip file from a function binary.
254/// The binary inside the zip file is called `bootstrap` for function binaries.
255/// The binary inside the zip file is called by its name, and put inside the `extensions`
256/// directory, for extension binaries.
257pub fn zip_binary<BP: AsRef<Path>, DD: AsRef<Path>>(
258    binary_path: BP,
259    destination_directory: DD,
260    data: &BinaryData,
261    include: Option<Vec<String>>,
262) -> Result<BinaryArchive> {
263    let path = binary_path.as_ref();
264    let dir = destination_directory.as_ref();
265
266    let zipped = dir.join(data.zip_name());
267    debug!(?data, ?path, ?dir, ?zipped, "zipping binary");
268
269    let zipped_binary = File::create(&zipped)
270        .into_diagnostic()
271        .wrap_err_with(|| format!("failed to create zip file `{zipped:?}`"))?;
272
273    let mut file = File::open(path)
274        .into_diagnostic()
275        .wrap_err_with(|| format!("failed to open binary file `{path:?}`"))?;
276
277    let file_metadata = file
278        .metadata()
279        .into_diagnostic()
280        .wrap_err_with(|| format!("failed to get metadata from file `{path:?}`"))?;
281
282    let binary_modified_at = file_metadata
283        .modified()
284        .ok()
285        .or_else(|| file_metadata.created().ok());
286
287    let (binary_data, arch) = read_binary(&mut file, path)?;
288
289    let mut zip = ZipWriter::new(zipped_binary);
290    if let Some(files) = include {
291        include_files_in_zip(&mut zip, &files)?;
292    }
293
294    if let Some(parent) = data.parent_dir() {
295        let options = SimpleFileOptions::default();
296        zip.add_directory(parent, options)
297            .into_diagnostic()
298            .wrap_err_with(|| {
299                format!("failed to add directory `{parent}` to zip file `{zipped:?}`")
300            })?;
301    }
302
303    let binary_path_in_zip = data.binary_path_in_zip()?;
304
305    let options = zip_file_options(&file, path)?;
306
307    zip.start_file(binary_path_in_zip.to_string(), options)
308        .into_diagnostic()
309        .wrap_err_with(|| format!("failed to start zip file `{binary_path_in_zip:?}`"))?;
310    zip.write_all(&binary_data)
311        .into_diagnostic()
312        .wrap_err_with(|| format!("failed to write data into zip file `{binary_path_in_zip:?}`"))?;
313    zip.finish()
314        .into_diagnostic()
315        .wrap_err_with(|| format!("failed to finish zip file `{binary_path_in_zip:?}`"))?;
316
317    Ok(BinaryArchive::new(
318        zipped,
319        arch.to_string(),
320        BinaryModifiedAt(binary_modified_at),
321    ))
322}
323
324fn extract_data_from_zipped_binary<'a>(
325    zip_path: &'a Path,
326    binary_path: &'a str,
327) -> Result<(&'a str, BinaryModifiedAt)> {
328    let zipfile = File::open(zip_path).into_diagnostic()?;
329    let mut archive = ZipArchive::new(zipfile).into_diagnostic()?;
330
331    let mut file = archive.by_name(binary_path).into_diagnostic()?;
332    let (_, arch) = read_binary(&mut file, binary_path)?;
333
334    let metadata = file.get_metadata();
335    let mut last_modified_at = BinaryModifiedAt(None);
336    if let Some(dt) = metadata.last_modified_time {
337        let naive_dt: NaiveDateTime = dt.try_into().into_diagnostic()?;
338        last_modified_at = naive_dt.into();
339    }
340
341    Ok((arch, last_modified_at))
342}
343
344fn read_binary<'a, R: Read, D: std::fmt::Debug>(
345    entry: &mut R,
346    binary_path: D,
347) -> Result<(Vec<u8>, &'a str)> {
348    let mut binary_data = Vec::new();
349
350    entry
351        .read_to_end(&mut binary_data)
352        .into_diagnostic()
353        .wrap_err_with(|| format!("failed to read binary file `{binary_path:?}`"))?;
354
355    let object = ObjectFile::parse(&*binary_data)
356        .into_diagnostic()
357        .wrap_err("the provided function file is not a valid Linux binary")?;
358
359    let arch = match object.architecture() {
360        Architecture::Aarch64 => "arm64",
361        Architecture::X86_64 => "x86_64",
362        other => return Err(BuildError::InvalidBinaryArchitecture(other).into()),
363    };
364
365    Ok((binary_data, arch))
366}
367
368fn zip_file_options(file: &File, path: &Path) -> Result<SimpleFileOptions> {
369    let meta = file
370        .metadata()
371        .into_diagnostic()
372        .wrap_err_with(|| format!("failed to get metadata from file `{path:?}`"))?;
373    let perm = binary_permissions(&meta);
374    let mut options = SimpleFileOptions::default().unix_permissions(perm);
375    if let Some(mtime) = binary_mtime(&meta) {
376        options = options.last_modified_time(mtime);
377    }
378
379    Ok(options)
380}
381
382fn include_files_in_zip<W: Write + Seek>(
383    zip: &mut ZipWriter<W>,
384    files: &Vec<String>,
385) -> Result<()> {
386    let mut file_map = HashMap::with_capacity(files.len());
387    for file in files {
388        match file.split_once(':') {
389            None => file_map.insert(file.clone(), file.clone()),
390            Some((name, path)) => file_map.insert(name.into(), path.into()),
391        };
392    }
393
394    for (base, file) in file_map {
395        for entry in WalkDir::new(&file).into_iter().filter_map(|e| e.ok()) {
396            let path = entry.path();
397            let base = base.clone();
398            let file = file.clone();
399
400            let unix_base = convert_to_unix_path(Path::new(&base))
401                .ok_or_else(|| BuildError::InvalidUnixFileName(base.into()))?;
402            let unix_file = convert_to_unix_path(Path::new(&file))
403                .ok_or_else(|| BuildError::InvalidUnixFileName(file.into()))?;
404
405            let source_name = convert_to_unix_path(path)
406                .ok_or_else(|| BuildError::InvalidUnixFileName(path.into()))?;
407
408            let destination_name = source_name.replace(&unix_file, &unix_base);
409
410            if path.is_dir() {
411                trace!(%destination_name, "creating directory in zip file");
412
413                zip.add_directory(&destination_name, SimpleFileOptions::default())
414                    .into_diagnostic()
415                    .wrap_err_with(|| {
416                        format!("failed to add directory `{destination_name}` to zip file")
417                    })?;
418            } else {
419                trace!(%source_name, %destination_name, "including file in zip file");
420
421                let mut content = Vec::new();
422                let mut file = File::open(path)
423                    .into_diagnostic()
424                    .wrap_err_with(|| format!("failed to open file `{path:?}`"))?;
425                file.read_to_end(&mut content)
426                    .into_diagnostic()
427                    .wrap_err_with(|| format!("failed to read file `{path:?}`"))?;
428
429                let options = zip_file_options(&file, path)?;
430
431                zip.start_file(destination_name.clone(), options)
432                    .into_diagnostic()
433                    .wrap_err_with(|| {
434                        format!("failed to create zip content file `{destination_name:?}`")
435                    })?;
436
437                zip.write_all(&content)
438                    .into_diagnostic()
439                    .wrap_err_with(|| {
440                        format!("failed to write data into zip content file `{destination_name:?}`")
441                    })?;
442            }
443        }
444    }
445    Ok(())
446}
447
448fn binary_mtime(meta: &Metadata) -> Option<zip::DateTime> {
449    let Ok(modified) = meta.modified() else {
450        return None;
451    };
452
453    let dt: DateTime<Utc> = modified.into();
454    if let Ok(dt) = zip::DateTime::try_from(dt.naive_utc()) {
455        return Some(dt);
456    }
457
458    let Ok(created) = meta.created() else {
459        return None;
460    };
461
462    let dt: DateTime<Utc> = created.into();
463    zip::DateTime::try_from(dt.naive_utc()).ok()
464}
465
466#[cfg(unix)]
467fn binary_permissions(meta: &Metadata) -> u32 {
468    use std::os::unix::fs::PermissionsExt;
469    meta.permissions().mode()
470}
471
472#[cfg(not(unix))]
473fn binary_permissions(_meta: &Metadata) -> u32 {
474    0o755
475}
476
477#[cfg(target_os = "windows")]
478fn convert_to_unix_path(path: &Path) -> Option<String> {
479    let mut path_str = String::new();
480    for component in path.components() {
481        if let std::path::Component::Normal(os_str) = component {
482            if !path_str.is_empty() {
483                path_str.push('/');
484            }
485            path_str.push_str(os_str.to_str()?);
486        }
487    }
488    Some(path_str)
489}
490
491#[cfg(not(target_os = "windows"))]
492fn convert_to_unix_path(path: &Path) -> Option<String> {
493    path.to_str().map(String::from)
494}
495
496impl BinaryModifiedAt {
497    pub fn humanize(&self) -> String {
498        match self.0 {
499            Some(time) => HumanTime::from(time).to_string(),
500            None => "at unknown time".to_string(),
501        }
502    }
503}
504
505impl Serialize for BinaryModifiedAt {
506    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
507    where
508        S: Serializer,
509    {
510        match self.0 {
511            Some(time) => serializer.serialize_str(&HumanTime::from(time).to_string()),
512            None => serializer.serialize_none(),
513        }
514    }
515}
516
517#[cfg(test)]
518mod test {
519    use std::{
520        fs::{create_dir_all, remove_dir_all},
521        thread::sleep,
522        time::Duration,
523    };
524
525    use cargo_lambda_metadata::{cargo::load_metadata, fs::copy_without_replace};
526    use rstest::rstest;
527    use tempfile::TempDir;
528    use zip::ZipArchive;
529
530    use super::*;
531
532    #[test]
533    fn test_convert_to_unix_path() {
534        // On Windows, a PathBuff constructed from Path::join will have "\" as separator, while on Unix-like systems it will have "/"
535        let path = Path::new("extensions").join("test").join("filename");
536        assert_eq!(
537            "extensions/test/filename",
538            convert_to_unix_path(&path).expect("failed to convert file path")
539        );
540    }
541
542    #[test]
543    fn test_convert_to_unix_path_keep_original() {
544        let path = Path::new("extensions/test/filename");
545        assert_eq!(
546            "extensions/test/filename",
547            convert_to_unix_path(path).expect("failed to convert file path")
548        );
549    }
550
551    #[test]
552    fn test_convert_to_unix_path_empty_path() {
553        let path = Path::new("");
554        assert_eq!(
555            "",
556            convert_to_unix_path(path).expect("failed to convert file path")
557        );
558    }
559
560    #[rstest]
561    #[case("binary-x86-64", "x86_64")]
562    #[case("binary-arm64", "arm64")]
563    fn test_zip_funcion(#[case] name: &str, #[case] arch: &str) {
564        let data = BinaryData::new(name, false, false);
565        let bp = &format!("../../tests/binaries/{name}");
566        let dd = TempDir::with_prefix("cargo-lambda-").expect("failed to create temp dir");
567        let archive =
568            zip_binary(bp, dd.path(), &data, None).expect("failed to create binary archive");
569
570        assert_eq!(arch, archive.architecture);
571
572        let arch_path = dd.path().join("bootstrap.zip");
573        assert_eq!(arch_path, archive.path);
574
575        let file = File::open(arch_path).expect("failed to open zip file");
576        let mut zip = ZipArchive::new(file).expect("failed to open zip archive");
577
578        zip.by_name("bootstrap")
579            .expect("failed to find bootstrap in zip archive");
580    }
581
582    #[rstest]
583    #[case("binary-x86-64", "x86_64")]
584    #[case("binary-arm64", "arm64")]
585    fn test_zip_extension(#[case] name: &str, #[case] arch: &str) {
586        let data = BinaryData::new(name, true, false);
587
588        let bp = &format!("../../tests/binaries/{name}");
589        let dd = TempDir::with_prefix("cargo-lambda-").expect("failed to create temp dir");
590        let archive =
591            zip_binary(bp, dd.path(), &data, None).expect("failed to create binary archive");
592
593        assert_eq!(arch, archive.architecture);
594
595        let arch_path = dd.path().join(format!("{name}.zip"));
596        assert_eq!(arch_path, archive.path);
597
598        let file = File::open(arch_path).expect("failed to open zip file");
599        let mut zip = ZipArchive::new(file).expect("failed to open zip archive");
600
601        zip.by_name(&format!("extensions/{name}"))
602            .expect("failed to find bootstrap in zip archive");
603    }
604
605    #[rstest]
606    #[case("binary-x86-64", "x86_64")]
607    #[case("binary-arm64", "arm64")]
608    fn test_zip_internal_extension(#[case] name: &str, #[case] arch: &str) {
609        let data = BinaryData::new(name, true, true);
610
611        let bp = &format!("../../tests/binaries/{name}");
612        let dd = TempDir::with_prefix("cargo-lambda-").expect("failed to create temp dir");
613        let archive =
614            zip_binary(bp, dd.path(), &data, None).expect("failed to create binary archive");
615
616        assert_eq!(arch, archive.architecture);
617
618        let arch_path = dd.path().join(format!("{name}.zip"));
619        assert_eq!(arch_path, archive.path);
620
621        let file = File::open(arch_path).expect("failed to open zip file");
622        let mut zip = ZipArchive::new(file).expect("failed to open zip archive");
623
624        zip.by_name(name)
625            .unwrap_or_else(|_| panic!("failed to find {name} in zip archive"));
626    }
627
628    #[rstest]
629    #[case("binary-x86-64", "x86_64")]
630    #[case("binary-arm64", "arm64")]
631    fn test_zip_funcion_with_files(#[case] name: &str, #[case] arch: &str) {
632        let data = BinaryData::new(name, false, false);
633
634        let bp = &format!("../../tests/binaries/{name}");
635        let extra = vec!["Cargo.toml".into()];
636        let dd = TempDir::with_prefix("cargo-lambda-").expect("failed to create temp dir");
637        let archive =
638            zip_binary(bp, dd.path(), &data, Some(extra)).expect("failed to create binary archive");
639
640        assert_eq!(arch, archive.architecture);
641
642        let arch_path = dd.path().join("bootstrap.zip");
643        assert_eq!(arch_path, archive.path);
644
645        let file = File::open(arch_path).expect("failed to open zip file");
646        let mut zip = ZipArchive::new(file).expect("failed to open zip archive");
647
648        zip.by_name("bootstrap")
649            .expect("failed to find bootstrap in zip archive");
650
651        zip.by_name("Cargo.toml")
652            .expect("failed to find Cargo.toml in zip archive");
653    }
654
655    #[test]
656    fn test_consistent_hash() {
657        let data = BinaryData::new("binary-x86-64", false, false);
658
659        let bp = "../../tests/binaries/binary-x86-64";
660        let dd = TempDir::with_prefix("cargo-lambda-").expect("failed to create temp dir");
661
662        let archive1 =
663            zip_binary(bp, dd.path(), &data, None).expect("failed to create binary archive");
664
665        // Sleep to ensure that the mtime is different enough for the hash to change
666        sleep(Duration::from_secs(2));
667
668        let archive2 =
669            zip_binary(bp, dd.path(), &data, None).expect("failed to create binary archive");
670
671        assert_eq!(archive1.sha256().unwrap(), archive2.sha256().unwrap());
672    }
673
674    #[test]
675    fn test_create_binary_archive_with_base_path() {
676        let data = BinaryData::new("binary-x86-64", false, false);
677
678        let bp = "../../tests/binaries/binary-x86-64";
679        let dd = TempDir::with_prefix("cargo-lambda-").expect("failed to create temp dir");
680        let bsp = dd.path().join("binary-x86-64");
681
682        create_dir_all(&bsp).expect("failed to create dir");
683        copy_without_replace(bp, bsp.join("bootstrap")).expect("failed to copy bootstrap file");
684
685        let archive = create_binary_archive(None, &Some(dd.path()), &data, None)
686            .expect("failed to create binary archive");
687
688        let arch_path = bsp.join("bootstrap.zip");
689        assert_eq!(arch_path, archive.path);
690
691        let file = File::open(arch_path).expect("failed to open zip file");
692        let mut zip = ZipArchive::new(file).expect("failed to open zip archive");
693
694        zip.by_name("bootstrap")
695            .expect("failed to find bootstrap in zip archive");
696    }
697
698    #[test]
699    fn test_create_binary_archive_from_target() {
700        let data = BinaryData::new("binary-x86-64", false, false);
701
702        let bp = "../../tests/binaries/binary-x86-64";
703        let metadata = load_metadata("Cargo.toml").unwrap();
704        let target_dir =
705            target_dir_from_metadata(&metadata).unwrap_or_else(|_| PathBuf::from("target"));
706
707        let bsp = target_dir.join("lambda").join("binary-x86-64");
708
709        create_dir_all(&bsp).expect("failed to create dir");
710        copy_without_replace(bp, bsp.join("bootstrap")).expect("failed to copy bootstrap file");
711
712        let base_dir: Option<&Path> = None;
713        let archive = create_binary_archive(Some(&metadata), &base_dir, &data, None)
714            .expect("failed to create binary archive");
715
716        let arch_path = bsp.join("bootstrap.zip");
717        assert_eq!(arch_path, archive.path);
718
719        let file = File::open(arch_path).expect("failed to open zip file");
720        let mut zip = ZipArchive::new(file).expect("failed to open zip archive");
721
722        zip.by_name("bootstrap")
723            .expect("failed to find bootstrap in zip archive");
724
725        remove_dir_all(&bsp).expect("failed to delete dir");
726    }
727
728    #[test]
729    fn test_zip_funcion_with_parent_directories() {
730        let data = BinaryData::new("binary-x86-64", false, false);
731
732        let bp = "../../tests/binaries/binary-x86-64".to_string();
733        #[cfg(unix)]
734        let extra = vec!["source:../../tests/fixtures/examples-package".into()];
735        #[cfg(windows)]
736        let extra = vec!["source:..\\..\\tests\\fixtures\\examples-package".into()];
737
738        let dd = TempDir::with_prefix("cargo-lambda-").expect("failed to create temp dir");
739        let archive =
740            zip_binary(bp, dd.path(), &data, Some(extra)).expect("failed to create binary archive");
741
742        let arch_path = dd.path().join("bootstrap.zip");
743        assert_eq!(arch_path, archive.path);
744
745        let file = File::open(arch_path).expect("failed to open zip file");
746        let mut zip = ZipArchive::new(file).expect("failed to open zip archive");
747
748        zip.by_name("bootstrap")
749            .expect("failed to find bootstrap in zip archive");
750
751        zip.by_name("source/Cargo.toml")
752            .expect("failed to find Cargo.toml in zip archive");
753
754        zip.by_name("source/Cargo.lock")
755            .expect("failed to find Cargo.lock in zip archive");
756
757        zip.by_name("source/src/main.rs")
758            .expect("failed to find source/src/main.rs in zip archive");
759
760        zip.by_name("source/examples/example-lambda.rs")
761            .expect("failed to find source/examples/example-lambda.rs in zip archive");
762    }
763
764    #[rstest]
765    #[case("bootstrap.zip", "bootstrap")]
766    #[case("test-extension.zip", "extensions/test-extension")]
767    fn test_extract_data_from_zipped_bootstrap(#[case] name: &str, #[case] binary_path: &str) {
768        let zip_path = format!("../../tests/binaries/{name}");
769        let zip_path = Path::new(&zip_path);
770        let (arch, binary_modified_at) =
771            extract_data_from_zipped_binary(zip_path, binary_path).unwrap();
772
773        assert_eq!("x86_64", arch);
774        assertables::assert_some!(binary_modified_at.0);
775    }
776}