Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vortex-mod-mega"
version = "1.0.0"
version = "1.1.0"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
edition = "2021"
description = "MEGA WASM plugin for Vortex — resolves mega.nz files/folders and exposes AES-128-CTR + chunk MAC for host-side decryption"
license = "GPL-3.0"
Expand Down
2 changes: 1 addition & 1 deletion plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[plugin]
name = "vortex-mod-mega"
version = "1.0.0"
version = "1.1.0"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
category = "hoster"
author = "vortex-community"
description = "MEGA hoster — resolves mega.nz public file URLs and exposes AES-128-CTR streaming decryption with per-chunk MAC verification"
Expand Down
10 changes: 10 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub enum PluginError {

#[error("MAC mismatch: file integrity check failed")]
MacMismatch,

#[error("MEGA downloads are not supported yet: files are end-to-end encrypted (AES-128-CTR) and Vortex cannot decrypt them during download")]
DecryptionNotSupported,
}

#[cfg(test)]
Expand All @@ -59,6 +62,13 @@ mod tests {
assert!(PluginError::ApiError(-9).to_string().contains("-9"));
}

#[test]
fn decryption_not_supported_names_the_reason() {
let msg = PluginError::DecryptionNotSupported.to_string();
assert!(msg.contains("not supported yet"), "{msg}");
assert!(msg.contains("encrypted"), "{msg}");
}

#[test]
fn mac_mismatch_message_stable() {
assert_eq!(
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
//! Implements the plugin contract used by the Vortex plugin host:
//! - `can_handle(url)` → `"true"` / `"false"`
//! - `supports_playlist(url)` → `"true"` for folder URLs, `"false"` otherwise
//! - `extract_links(url)` → JSON metadata for a single MEGA file
//! - `resolve_stream_url(input)` → encrypted CDN URL (host fetches + decrypts)
//! - `extract_links(url)` / `resolve_stream_url(input)` → refused with an

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: According to linked Linear issue MAT-136 (R-04), unavailable MEGA downloads must be presented honestly; this new contract is not propagated to the README or WASM smoke test, leaving users with the old capability description and CI with assertions for the superseded behavior. Updating the contract documentation and test expectations alongside this change would keep the release surface consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib.rs, line 6:

<comment>According to linked Linear issue MAT-136 (R-04), unavailable MEGA downloads must be presented honestly; this new contract is not propagated to the README or WASM smoke test, leaving users with the old capability description and CI with assertions for the superseded behavior. Updating the contract documentation and test expectations alongside this change would keep the release surface consistent.</comment>

<file context>
@@ -3,8 +3,8 @@
 //! - `supports_playlist(url)` → `"true"` for folder URLs, `"false"` otherwise
-//! - `extract_links(url)` → JSON metadata for a single MEGA file
-//! - `resolve_stream_url(input)` → encrypted CDN URL (host fetches + decrypts)
+//! - `extract_links(url)` / `resolve_stream_url(input)` → refused with an
+//!   explicit error until the host can decrypt MEGA payloads (MAT-136 R-04)
 //!
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — CI was red on exactly this: folder_e2e and the wasm_smoke extraction test still asserted the old contract. Fixed in 650b0c6: the README contract table and status now describe the refusal, wasm_smoke asserts both exports refuse a valid file URL, and folder_e2e is slimmed to a wasm-boundary check that folder URLs refuse too (the synthetic decryption fixture stays in git history for the rewire).

//! explicit error until the host can decrypt MEGA payloads (MAT-136 R-04)
//!
//! Network access is delegated to the host via `http_request`. All parsing
//! and crypto is pure (`url_matcher.rs`, `key_parser.rs`, `crypto.rs`,
Expand Down
76 changes: 13 additions & 63 deletions src/plugin_api.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
//! WASM-only entry points: `#[plugin_fn]` exports + `#[host_fn]` imports.
//! WASM-only entry points: `#[plugin_fn]` exports.

use extism_pdk::*;

use crate::api_client::{
build_folder_request, build_g_request, parse_g_response, parse_http_response, HttpResponse,
};
use crate::error::PluginError;
use crate::key_parser::{parse_file_key, parse_folder_key};
use crate::node_parser::parse_folder_listing;
use crate::url_matcher::UrlKind;
use crate::{
build_file_extract_links, build_folder_extract_links, ensure_supported_url, handle_can_handle,
handle_supports_playlist,
};

#[host_fn]
extern "ExtismHost" {
fn http_request(req: String) -> String;
}
use crate::{ensure_supported_url, handle_can_handle, handle_supports_playlist};

#[plugin_fn]
pub fn can_handle(url: String) -> FnResult<String> {
Expand All @@ -29,29 +15,17 @@ pub fn supports_playlist(url: String) -> FnResult<String> {
Ok(handle_supports_playlist(&url))
}

// MAT-136 R-04: MEGA CDN bytes are AES-128-CTR ciphertext and the Vortex host
// has no decryption pipeline yet, so a "successful" download would write
// unreadable bytes to disk. Both download-facing exports refuse with an
// explicit error until host-side decryption ships; the resolution and crypto
// code stays in the library (api_client, crypto, key_parser, node_parser)
// ready to rewire — see git history for the previous wiring.

#[plugin_fn]
pub fn extract_links(url: String) -> FnResult<String> {
let kind = ensure_supported_url(&url).map_err(error_to_fn_error)?;
let response_json = match kind {
UrlKind::File { id, key_b64 } => {
let key = parse_file_key(&key_b64).map_err(error_to_fn_error)?;
let resolution = call_g_command(&id)?;
let response = build_file_extract_links(&url, &id, &key, resolution, None);
serde_json::to_string(&response)
.map_err(|e| error_to_fn_error(PluginError::SerdeJson(e)))?
}
UrlKind::Folder { id, key_b64 } => {
let folder_key = parse_folder_key(&key_b64).map_err(error_to_fn_error)?;
let body = call_f_command(&id)?;
let children =
parse_folder_listing(&body, &folder_key.aes_key).map_err(error_to_fn_error)?;
let response = build_folder_extract_links(children);
serde_json::to_string(&response)
.map_err(|e| error_to_fn_error(PluginError::SerdeJson(e)))?
}
UrlKind::Unknown => unreachable!("ensure_supported_url filtered Unknown"),
};
Ok(response_json)
ensure_supported_url(&url).map_err(error_to_fn_error)?;
Err(error_to_fn_error(PluginError::DecryptionNotSupported))
}

#[plugin_fn]
Expand All @@ -62,32 +36,8 @@ pub fn resolve_stream_url(input: String) -> FnResult<String> {
}
let params: Input =
serde_json::from_str(&input).map_err(|e| error_to_fn_error(PluginError::SerdeJson(e)))?;
let kind = ensure_supported_url(&params.url).map_err(error_to_fn_error)?;
match kind {
UrlKind::File { id, .. } => {
let resolution = call_g_command(&id)?;
Ok(resolution.direct_url)
}
_ => Err(error_to_fn_error(PluginError::UnsupportedUrl(params.url))),
}
}

fn call_g_command(file_id: &str) -> FnResult<crate::api_client::MegaFileResolution> {
let req = build_g_request(file_id).map_err(error_to_fn_error)?;
// SAFETY: see invariants in vortex-mod-mediafire's plugin_api.rs.
let raw = unsafe { http_request(req)? };
let resp: HttpResponse = parse_http_response(&raw).map_err(error_to_fn_error)?;
let body = resp.into_success_body().map_err(error_to_fn_error)?;
parse_g_response(&body).map_err(error_to_fn_error)
}

fn call_f_command(folder_id: &str) -> FnResult<String> {
let req = build_folder_request(folder_id).map_err(error_to_fn_error)?;
// SAFETY: same invariants as `call_g_command` — host-resolved import,
// String-marshalled JSON envelope, http capability checked at load.
let raw = unsafe { http_request(req)? };
let resp: HttpResponse = parse_http_response(&raw).map_err(error_to_fn_error)?;
resp.into_success_body().map_err(error_to_fn_error)
ensure_supported_url(&params.url).map_err(error_to_fn_error)?;
Err(error_to_fn_error(PluginError::DecryptionNotSupported))
}

fn error_to_fn_error(err: PluginError) -> WithReturnCode<extism_pdk::Error> {
Expand Down
Loading