Skip to content

Commit

Permalink
Url util functions (#367)
Browse files Browse the repository at this point in the history
* URL util functions

* Update API docs

* clippy

* Fix markdown
  • Loading branch information
richarddavison authored May 8, 2024
1 parent b2649cd commit f796a5d
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 41 deletions.
6 changes: 4 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,6 @@ export class URL {
}
```

### TODO, URL see tracking [ticket](https://github.com/awslabs/llrt/issues/303):

```typescript
// Additional utilities in the URL module
export function domainToASCII(domain: string): string;
Expand All @@ -202,11 +200,15 @@ export function fileURLToPath(url: string | URL): string;

export function pathToFileURL(path: string): URL;

export function format(url: string | URL, options?: { fragment?: boolean, unicode?: boolean, auth?: boolean
}): string;

export function urlToHttpOptions(url: URL): {
protocol?: string;
hostname?: string;
port?: string;
path?: string;
...
};
```

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions llrt_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ rand = "0.8.5"
uname = "0.1.1"
flate2 = { version = "1.0.30", features = ["zlib-ng"], default-features = false }
brotlic = "0.8.2"
idna = "0.5.0"

[build-dependencies]
rquickjs = { version = "0.5.1", features = [
Expand Down
168 changes: 146 additions & 22 deletions llrt_core/src/modules/http/url.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::{path::PathBuf, str::FromStr};

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{
atom::PredefinedAtom,
class::{Trace, Tracer},
function::Opt,
prelude::This,
Class, Coerced, Ctx, Exception, FromJs, Function, Result, Value,
Class, Coerced, Ctx, Exception, FromJs, Function, Object, Result, Value,
};
use url::Url;

Expand Down Expand Up @@ -90,27 +92,7 @@ impl<'js> URL<'js> {
}

pub fn to_string(&self) -> String {
let search = search_params_to_string(&self.search_params);
let hash = &self.hash;
let hash = if !hash.is_empty() {
format!("#{}", &hash)
} else {
String::from("")
};
let mut user_info = String::new();
if !self.username.is_empty() {
user_info.push_str(&self.username);
if !self.password.is_empty() {
user_info.push(':');
user_info.push_str(&self.password)
}
user_info.push('@')
}

format!(
"{}://{}{}{}{}{}",
&self.protocol, user_info, &self.host, &self.pathname, &search, &hash
)
self.format(true, true, true, false)
}

#[qjs(get)]
Expand Down Expand Up @@ -309,6 +291,47 @@ impl<'js> URL<'js> {
self.host.clone_from(&self.hostname);
}
}

fn format(
&self,
include_auth: bool,
include_fragment: bool,
include_search: bool,
unicode_encode: bool,
) -> String {
let search = if include_search {
search_params_to_string(&self.search_params)
} else {
String::from("")
};
let hash = &self.hash;
let hash = if include_fragment && !hash.is_empty() {
format!("#{}", &hash)
} else {
String::from("")
};

let mut user_info = String::new();
if include_auth && !self.username.is_empty() {
user_info.push_str(&self.username);
if !self.password.is_empty() {
user_info.push(':');
user_info.push_str(&self.password)
}
user_info.push('@')
}

let host = if unicode_encode {
domain_to_unicode(&self.host)
} else {
self.host.clone()
};

format!(
"{}://{}{}{}{}{}",
&self.protocol, user_info, host, &self.pathname, &search, &hash
)
}
}

fn filtered_port(protocol: &str, port: &str) -> Option<String> {
Expand Down Expand Up @@ -357,3 +380,104 @@ fn split_colon<'js>(ctx: &Ctx, s: &'js str) -> Result<(&'js str, &'js str)> {
}
Ok((first, second))
}

pub fn url_to_http_options<'js>(ctx: Ctx<'js>, url: Class<'js, URL<'js>>) -> Result<Object<'js>> {
let obj = Object::new(ctx)?;

let url = url.borrow();

let port = url.port();
let username = url.username();
let search = url.search();
let hash = url.hash();

obj.set("protocol", url.protocol())?;
obj.set("hostname", url.hostname())?;

if !hash.is_empty() {
obj.set("hash", url.hash())?;
}
if !search.is_empty() {
obj.set("search", url.search())?;
}

obj.set("pathname", url.pathname())?;
obj.set("path", format!("{}{}", url.pathname(), url.search()))?;
obj.set("href", url.href())?;

if !username.is_empty() {
obj.set("auth", format!("{}:{}", username, url.password()))?;
}

if !port.is_empty() {
obj.set("port", url.port())?;
}

Ok(obj)
}

pub fn domain_to_unicode(domain: &str) -> String {
let (url, result) = idna::domain_to_unicode(domain);
if result.is_err() {
return String::from("");
}
url
}

pub fn domain_to_ascii(domain: &str) -> String {
idna::domain_to_ascii(domain).unwrap_or_default()
}

//options are ignored, no windows support yet
pub fn path_to_file_url<'js>(ctx: Ctx<'js>, path: String, _: Opt<Value>) -> Result<URL<'js>> {
let url = Url::from_file_path(path).unwrap();
URL::create(ctx, url)
}

//options are ignored, no windows support yet
pub fn file_url_to_path<'js>(ctx: Ctx<'js>, url: Value<'js>) -> Result<String> {
let url_string = if let Ok(url) = Class::<URL>::from_value(url.clone()) {
url.borrow().to_string()
} else {
url.get::<Coerced<String>>()?.to_string()
};

let path = if let Some(path) = &url_string.strip_prefix("file://") {
path.to_string()
} else {
url_string
};

Ok(PathBuf::from_str(&path)
.or_throw(&ctx)?
.to_string_lossy()
.to_string())
}

pub fn url_format<'js>(url: Class<'js, URL<'js>>, options: Opt<Value<'js>>) -> Result<String> {
let mut fragment = true;
let mut unicode = false;
let mut auth = true;
let mut search = true;

// Parse options if provided
if let Some(options) = options.0 {
if options.is_object() {
let options = options.as_object().unwrap();
if let Some(value) = options.get("fragment")? {
fragment = value;
}
if let Ok(value) = options.get("unicode") {
unicode = value;
}
if let Ok(value) = options.get("auth") {
auth = value;
}
if let Ok(value) = options.get("search") {
search = value
}
}
}

Ok(url.borrow().format(auth, fragment, search, unicode))
}
7 changes: 5 additions & 2 deletions llrt_core/src/modules/http/url_search_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ impl URLSearchParams {
let mut string = String::with_capacity(self.params.len() * 2);
for (i, (key, value)) in self.params.iter().enumerate() {
string.push_str(&escape(key));
string.push('=');
string.push_str(&escape(value));
if !value.is_empty() {
string.push('=');
string.push_str(&escape(value));
}

if i < length - 1 {
string.push('&');
}
Expand Down
26 changes: 24 additions & 2 deletions llrt_core/src/modules/url.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use rquickjs::{
function::Constructor,
function::{Constructor, Func},
module::{Declarations, Exports, ModuleDef},
Ctx, Result,
};

use crate::{module_builder::ModuleInfo, modules::module::export_default};

use super::http::url::{
domain_to_ascii, domain_to_unicode, file_url_to_path, path_to_file_url, url_format,
url_to_http_options,
};
pub struct UrlModule;

impl ModuleDef for UrlModule {
fn declare(declare: &mut Declarations) -> Result<()> {
declare.declare(stringify!(URL))?;
declare.declare(stringify!(URLSearchParams))?;

declare.declare("urlToHttpOptions")?;
declare.declare("domainToUnicode")?;
declare.declare("domainToASCII")?;
declare.declare("fileURLToPath")?;
declare.declare("pathToFileURL")?;
declare.declare("format")?;
declare.declare("default")?;
Ok(())
}
Expand All @@ -26,6 +36,18 @@ impl ModuleDef for UrlModule {
export_default(ctx, exports, |default| {
default.set(stringify!(URL), url)?;
default.set(stringify!(URLSearchParams), url_search_params)?;
default.set("urlToHttpOptions", Func::from(url_to_http_options))?;
default.set(
"domainToUnicode",
Func::from(|domain: String| domain_to_unicode(&domain)),
)?;
default.set(
"domainToASCII",
Func::from(|domain: String| domain_to_ascii(&domain)),
)?;
default.set("fileURLToPath", Func::from(file_url_to_path))?;
default.set("pathToFileURL", Func::from(path_to_file_url))?;
default.set("format", Func::from(url_format))?;
Ok(())
})?;

Expand Down
9 changes: 5 additions & 4 deletions llrt_core/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,10 +619,11 @@ fn init(ctx: &Ctx<'_>, module_names: HashSet<&'static str>) -> Result<()> {
"require",
Func::from(move |ctx, specifier: String| -> Result<Value> {
let LifetimeArgs(ctx) = LifetimeArgs(ctx);
let specifier: String = specifier
.strip_prefix("node:")
.unwrap_or(specifier.as_str())
.into();
let specifier = if let Some(striped_specifier) = &specifier.strip_prefix("node:") {
striped_specifier.to_string()
} else {
specifier
};
let import_name = if module_names.contains(specifier.as_str())
|| BYTECODE_CACHE.contains_key(&specifier)
|| specifier.starts_with('/')
Expand Down
Loading

0 comments on commit f796a5d

Please sign in to comment.