diff --git a/index.d.ts b/index.d.ts index e93eb75..dd95831 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,10 +5,29 @@ export function overwriteConfig(adminPort: number, configPath: string, keystoreConnectionUrl: string, bootstrapServerUrl: string, signalingServerUrl: string, allowedOrigin: string, useDpki: boolean, iceServerUrls?: Array | undefined | null, keystoreInProcEnvironmentDir?: string | undefined | null): string export function defaultConductorConfig(adminPort: number, conductorEnvironmentPath: string, keystoreConnectionUrl: string, bootstrapServerUrl: string, signalingServerUrl: string, allowedOrigin: string, useDpki: boolean, iceServerUrls?: Array | undefined | null, keystoreInProcEnvironmentDir?: string | undefined | null): string +export interface HappAndUiHashes { + happSha256: string + webhappSha256?: string + uiSha256?: string +} +export interface StoredHappPathAndHashes { + happPath: string + happSha256: string + webhappSha256?: string + uiSha256?: string +} export function happBytesWithCustomProperties(happPath: string, properties: Record): Promise> -export function saveHappOrWebhapp(happOrWebHappPath: string, uisDir: string, happsDir: string): Promise -/** Checks that the happ or webhapp is of the correct format */ -export function validateHappOrWebhapp(happOrWebhappBytes: Array): Promise +/** + * Saves a happ or a webhapp file. If a uis_dir is specified and it is a webhapp, + * then the UI will be stored in [uis_dir]/[sha 256 of UI]/assets. If no uis_dir + * is specified, only the happ file will be stored. + */ +export function saveHappOrWebhapp(happOrWebHappPath: string, happsDir: string, uisDir?: string | undefined | null): Promise +/** + * Checks that the happ or webhapp is of the correct format + * WARNING: The decoding and encoding of the happ bytes seems to affect happ's sha256 hash. + */ +export function validateHappOrWebhapp(happOrWebhappBytes: Array): Promise export interface ZomeCallUnsignedNapi { cellId: Array> zomeName: string diff --git a/src/decode_webapp.rs b/src/decode_webapp.rs index e51daf6..ab5a1bb 100644 --- a/src/decode_webapp.rs +++ b/src/decode_webapp.rs @@ -6,6 +6,23 @@ use std::collections::HashMap; use std::fs; use std::path::PathBuf; +use crate::utils::hash_bytes_sha256; + +#[napi(object)] +pub struct HappAndUiHashes { + pub happ_sha256: String, + pub webhapp_sha256: Option, + pub ui_sha256: Option, +} + +#[napi(object)] +pub struct StoredHappPathAndHashes { + pub happ_path: String, + pub happ_sha256: String, + pub webhapp_sha256: Option, + pub ui_sha256: Option, +} + #[napi] pub async fn happ_bytes_with_custom_properties( happ_path: String, @@ -35,7 +52,7 @@ pub async fn happ_bytes_with_custom_properties( println!("yaml_value: {:?}", yaml_value); let yaml_properties = YamlProperties::from(yaml_value); role_manifest.dna.modifiers.properties = Some(yaml_properties); - }, + } None => { role_manifest.dna.modifiers.properties = None; } @@ -63,55 +80,27 @@ pub async fn happ_bytes_with_custom_properties( Ok(app_bundle_bytes) } +/// Saves a happ or a webhapp file. If a uis_dir is specified and it is a webhapp, +/// then the UI will be stored in [uis_dir]/[sha 256 of UI]/assets. If no uis_dir +/// is specified, only the happ file will be stored. #[napi] pub async fn save_happ_or_webhapp( happ_or_web_happ_path: String, - uis_dir: String, happs_dir: String, -) -> napi::Result { + uis_dir: Option, +) -> napi::Result { let happ_or_webhapp_bytes = fs::read(happ_or_web_happ_path)?; - let (app_bundle, maybe_ui_and_webhapp_hash) = match WebAppBundle::decode(&happ_or_webhapp_bytes) - { + match WebAppBundle::decode(&happ_or_webhapp_bytes) { Ok(web_app_bundle) => { - let mut hasher = Sha256::new(); - hasher.update(happ_or_webhapp_bytes); - let web_happ_hash = hex::encode(hasher.finalize()); + let web_happ_hash = hash_bytes_sha256(happ_or_webhapp_bytes); + // extracting ui.zip bytes let web_ui_zip_bytes = web_app_bundle.web_ui_zip_bytes().await.map_err(|e| { napi::Error::from_reason(format!("Failed to extract ui zip bytes: {}", e)) })?; - let mut hasher = Sha256::new(); - hasher.update(web_ui_zip_bytes.clone().into_owned().into_inner()); - let ui_hash = hex::encode(hasher.finalize()); - - let ui_target_dir = PathBuf::from(uis_dir).join(ui_hash.clone()).join("assets"); - if !path_exists(&ui_target_dir) { - fs::create_dir_all(&ui_target_dir)?; - } - - let ui_zip_path = PathBuf::from(ui_target_dir.clone()).join("ui.zip"); - - // unzip and store UI - fs::write( - ui_zip_path.clone(), - web_ui_zip_bytes.into_owned().into_inner(), - ) - .map_err(|e| { - napi::Error::from_reason(format!("Failed to write Web UI Zip file: {}", e)) - })?; - - let file = fs::File::open(ui_zip_path.clone()).map_err(|e| { - napi::Error::from_reason(format!("Failed to read Web UI Zip file: {}", e)) - })?; - - unzip_file(file, ui_target_dir.into()) - .map_err(|e| napi::Error::from_reason(format!("Failed to unzip ui.zip: {}", e)))?; - - fs::remove_file(ui_zip_path).map_err(|e| { - napi::Error::from_reason(format!("Failed to remove ui.zip after unzipping: {}", e)) - })?; + let ui_hash = hash_bytes_sha256(web_ui_zip_bytes.clone().into_owned().into_inner()); // extracting happ bundle let app_bundle = web_app_bundle.happ_bundle().await.map_err(|e| { @@ -121,51 +110,104 @@ pub async fn save_happ_or_webhapp( )) })?; - (app_bundle, Some((ui_hash, web_happ_hash))) + let app_bundle_bytes = app_bundle.encode().map_err(|e| { + napi::Error::from_reason(format!("Failed to encode happ to bytes: {}", e)) + })?; + + let happ_hash = hash_bytes_sha256(app_bundle_bytes); + + let happ_path = PathBuf::from(happs_dir).join(format!("{}.happ", happ_hash)); + let happ_path_string = happ_path + .as_os_str() + .to_str() + .ok_or("Failed to convert happ path to string") + .map_err(|e| napi::Error::from_reason(e))?; + + // Store UI if uis_dir is specified + match uis_dir { + Some(dir) => { + let ui_target_dir = PathBuf::from(dir).join(ui_hash.clone()).join("assets"); + if !path_exists(&ui_target_dir) { + fs::create_dir_all(&ui_target_dir)?; + } + + let ui_zip_path = PathBuf::from(ui_target_dir.clone()).join("ui.zip"); + + // unzip and store UI + fs::write( + ui_zip_path.clone(), + web_ui_zip_bytes.into_owned().into_inner(), + ) + .map_err(|e| { + napi::Error::from_reason(format!("Failed to write Web UI Zip file: {}", e)) + })?; + + let file = fs::File::open(ui_zip_path.clone()).map_err(|e| { + napi::Error::from_reason(format!("Failed to read Web UI Zip file: {}", e)) + })?; + + unzip_file(file, ui_target_dir.into()).map_err(|e| { + napi::Error::from_reason(format!("Failed to unzip ui.zip: {}", e)) + })?; + + fs::remove_file(ui_zip_path).map_err(|e| { + napi::Error::from_reason(format!( + "Failed to remove ui.zip after unzipping: {}", + e + )) + })?; + } + None => (), + } + + app_bundle.write_to_file(&happ_path).await.map_err(|e| { + napi::Error::from_reason(format!("Failed to write .happ file: {}", e)) + })?; + + Ok(StoredHappPathAndHashes { + happ_path: happ_path_string.into(), + happ_sha256: happ_hash, + webhapp_sha256: Some(web_happ_hash), + ui_sha256: Some(ui_hash), + }) } Err(_) => { let app_bundle = AppBundle::decode(&happ_or_webhapp_bytes).map_err(|e| { napi::Error::from_reason(format!("Failed to decode happ file: {}", e)) })?; - (app_bundle, None) - } - }; - let mut hasher = Sha256::new(); - let app_bundle_bytes = app_bundle - .encode() - .map_err(|e| napi::Error::from_reason(format!("Failed to encode happ to bytes: {}", e)))?; - hasher.update(app_bundle_bytes); - let happ_hash = hex::encode(hasher.finalize()); - let happ_path = PathBuf::from(happs_dir).join(format!("{}.happ", happ_hash)); - - app_bundle - .write_to_file(&happ_path) - .await - .map_err(|e| napi::Error::from_reason(format!("Failed to write .happ file: {}", e)))?; - - let happ_path_string = happ_path.as_os_str().to_str(); - match happ_path_string { - Some(str) => match maybe_ui_and_webhapp_hash { - Some((ui_hash, web_happ_hash)) => Ok(format!( - "{}${}${}${}", - str.to_string(), - happ_hash, - ui_hash, - web_happ_hash, - )), - None => Ok(format!("{}${}", str.to_string(), happ_hash)), - }, - None => Err(napi::Error::from_reason( - "Failed to convert happ path to string.", - )), + let app_bundle_bytes = app_bundle.encode().map_err(|e| { + napi::Error::from_reason(format!("Failed to encode happ to bytes: {}", e)) + })?; + + let happ_hash = hash_bytes_sha256(app_bundle_bytes); + + let happ_path = PathBuf::from(happs_dir).join(format!("{}.happ", happ_hash)); + let happ_path_string = happ_path + .as_os_str() + .to_str() + .ok_or("Failed to convert happ path to string") + .map_err(|e| napi::Error::from_reason(e))?; + + app_bundle.write_to_file(&happ_path).await.map_err(|e| { + napi::Error::from_reason(format!("Failed to write .happ file: {}", e)) + })?; + Ok(StoredHappPathAndHashes { + happ_path: happ_path_string.into(), + happ_sha256: happ_hash, + webhapp_sha256: None, + ui_sha256: None, + }) + } } } /// Checks that the happ or webhapp is of the correct format /// WARNING: The decoding and encoding of the happ bytes seems to affect happ's sha256 hash. #[napi] -pub async fn validate_happ_or_webhapp(happ_or_webhapp_bytes: Vec) -> napi::Result { +pub async fn validate_happ_or_webhapp( + happ_or_webhapp_bytes: Vec, +) -> napi::Result { let (app_bundle, maybe_ui_and_webhapp_hash) = match WebAppBundle::decode(&happ_or_webhapp_bytes) { Ok(web_app_bundle) => { @@ -207,10 +249,16 @@ pub async fn validate_happ_or_webhapp(happ_or_webhapp_bytes: Vec) -> napi::R let happ_hash = hex::encode(hasher.finalize()); match maybe_ui_and_webhapp_hash { - Some((ui_hash, web_happ_hash)) => { - Ok(format!("{}${}${}", happ_hash, ui_hash, web_happ_hash)) - } - None => Ok(format!("{}", happ_hash)), + Some((ui_hash, web_happ_hash)) => Ok(HappAndUiHashes { + happ_sha256: happ_hash, + webhapp_sha256: Some(web_happ_hash), + ui_sha256: Some(ui_hash), + }), + None => Ok(HappAndUiHashes { + happ_sha256: happ_hash, + webhapp_sha256: None, + ui_sha256: None, + }), } } diff --git a/src/utils.rs b/src/utils.rs index ae85d6c..eb19bbe 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,13 @@ +use sha2::{Digest, Sha256}; + + pub fn vec_to_arr(v: Vec) -> [T; N] { v.try_into() .unwrap_or_else(|v: Vec| panic!("Expected a Vec of length {} but it was {}", N, v.len())) } + +pub fn hash_bytes_sha256(bytes: Vec) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hex::encode(hasher.finalize()) +} \ No newline at end of file