Merge branch 'v2' into feat/cef

This commit is contained in:
Lucas Nogueira
2026-02-18 12:43:41 -03:00
19 changed files with 227 additions and 66 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"fs": minor
"fs-js": minor
---
Add `encoding` option for `readTextFile` and `readTextFileLines`
+6
View File
@@ -0,0 +1,6 @@
---
"http": patch
"http-js": patch
---
Correct Response header initialization to support cloning and ensure Set-Cookie visibility.
-6
View File
@@ -1,6 +0,0 @@
---
"deep-link": patch
"deep-link-js": patch
---
Fix runtime deep link registration failing on Linux when the app path has spaces.
@@ -1,9 +0,0 @@
---
"single-instance": minor:fix
---
**Breaking Change:** On Linux, the DBus ID/name will now be `<bundle-id>.SingleInstance` instead of `org.<bundle_id_underscores>.SingleInstance` to follow DBus specifications.
This will break the single-instance mechanism across different app versions if the app was installed multiple times.
Added `dbus_id` builder method, which can be used to restore previous behavior. For a bundle identifier of `com.tauri.my-example` this would be `dbus_id("org.com_tauri_my_example")`.
Generated
+2 -2
View File
@@ -6683,7 +6683,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.6"
version = "2.4.7"
dependencies = [
"dunce",
"plist",
@@ -6961,7 +6961,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.7"
version = "2.4.0"
dependencies = [
"semver",
"serde",
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.4.7]
- [`8374e997`](https://github.com/tauri-apps/plugins-workspace/commit/8374e997b82c95516fc0c1f6d665d9fc3b52edf8) ([#3258](https://github.com/tauri-apps/plugins-workspace/pull/3258) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fix runtime deep link registration failing on Linux when the app path has spaces.
## \[2.4.6]
- [`28048039`](https://github.com/tauri-apps/plugins-workspace/commit/28048039496e84b46847c008416d341f1349e30e) ([#3143](https://github.com/tauri-apps/plugins-workspace/pull/3143) by [@Tunglies](https://github.com/tauri-apps/plugins-workspace/../../Tunglies)) Fix clippy warnings. No user facing changes.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-deep-link"
version = "2.4.6"
version = "2.4.7"
description = "Set your Tauri application as the default handler for an URL"
authors = { workspace = true }
license = { workspace = true }
@@ -1,5 +1,11 @@
# Changelog
## \[2.2.10]
### Dependencies
- Upgraded to `deep-link-js@2.4.7`
## \[2.2.9]
### Dependencies
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "deep-link-example",
"private": true,
"version": "2.2.9",
"version": "2.2.10",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,7 +11,7 @@
},
"dependencies": {
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-deep-link": "2.4.6"
"@tauri-apps/plugin-deep-link": "2.4.7"
},
"devDependencies": {
"@tauri-apps/cli": "2.10.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-deep-link",
"version": "2.4.6",
"version": "2.4.7",
"description": "Set your Tauri application as the default handler for an URL",
"license": "MIT OR Apache-2.0",
"authors": [
File diff suppressed because one or more lines are too long
+12 -7
View File
@@ -723,6 +723,8 @@ async function readDir(
interface ReadFileOptions {
/** Base directory for `path` */
baseDir?: BaseDirectory
/** Text encoding to use when reading a text file. Defaults to 'utf-8'. */
encoding?: string
}
/**
@@ -753,7 +755,7 @@ async function readFile(
}
/**
* Reads and returns the entire contents of a file as UTF-8 string.
* Reads and returns the entire contents of a file as a string using the specified encoding (default: UTF-8).
* @example
* ```typescript
* import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
@@ -777,11 +779,11 @@ async function readTextFile(
const bytes = arr instanceof ArrayBuffer ? arr : Uint8Array.from(arr)
return new TextDecoder().decode(bytes)
return new TextDecoder(options?.encoding ?? 'utf-8').decode(bytes)
}
/**
* Returns an async {@linkcode AsyncIterableIterator} over the lines of a file as UTF-8 string.
* Returns an async {@linkcode AsyncIterableIterator} over the lines of a file, decoded using the specified encoding (default: UTF-8).
* @example
* ```typescript
* import { readTextFileLines, BaseDirectory } from '@tauri-apps/plugin-fs';
@@ -810,10 +812,15 @@ async function readTextFileLines(
rid: null as number | null,
async next(): Promise<IteratorResult<string>> {
const decoder = new TextDecoder(options?.encoding ?? 'utf-8')
if (this.rid === null) {
// Use the normalized encoding label for options.
const encoding = decoder.encoding
this.rid = await invoke<number>('plugin:fs|read_text_file_lines', {
path: pathStr,
options
options: options != null ? { ...options, encoding } : undefined
})
}
@@ -838,9 +845,7 @@ async function readTextFileLines(
return { value: null, done }
}
const line = new TextDecoder().decode(
bytes.slice(0, bytes.byteLength - 1)
)
const line = decoder.decode(bytes.slice(0, bytes.byteLength - 1))
return {
value: line,
+144 -25
View File
@@ -393,6 +393,14 @@ pub async fn read_file<R: Runtime>(
.await
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadTextFileOptions {
#[serde(flatten)]
base: BaseOptions,
encoding: Option<String>,
}
// TODO, remove in v3, rely on `read_file` command instead
#[tauri::command]
pub async fn read_text_file<R: Runtime>(
@@ -419,7 +427,7 @@ pub fn read_text_file_lines<R: Runtime>(
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
options: Option<ReadTextFileOptions>,
) -> CommandResult<ResourceId> {
let resolved_path = resolve_path(
"read-text-file-lines",
@@ -427,7 +435,7 @@ pub fn read_text_file_lines<R: Runtime>(
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
options.as_ref().and_then(|o| o.base.base_dir),
)?;
let file = File::open(&resolved_path).map_err(|e| {
@@ -437,12 +445,43 @@ pub fn read_text_file_lines<R: Runtime>(
)
})?;
let encoding = options.as_ref().and_then(|o| o.encoding.as_deref());
let (lf_bytes, cr_bytes) = lf_cr_bytes_for_encoding_label(encoding);
let lines = BufReader::new(file);
let rid = webview.resources_table().add(StdLinesResource::new(lines));
let rid = webview
.resources_table()
.add(StdLinesResource::new(lines, lf_bytes, cr_bytes));
Ok(rid)
}
/// Returns the byte sequences for LF (`\n`) and CR (`\r`) in the encoding label.
///
/// The provided encoding label must be a normalized, lowercase string,
/// such as one obtained via `(new TextDecoder(encoding)).encoding`.
///
/// <https://developer.mozilla.org/ja/docs/Web/API/Encoding_API/Encodings>
fn lf_cr_bytes_for_encoding_label(label: Option<&str>) -> (Vec<u8>, Vec<u8>) {
// Defaults to utf-8
// https://developer.mozilla.org/ja/docs/Web/API/TextDecoder/TextDecoder#label
let label = label.unwrap_or("utf-8");
// Currently, according to the Web Standard,
// the ASCII-incompatible encodings are UTF-16LE/BE and ISO-2022-JP.
// However, ISO-2022-JP can still detect line breaks in the same way as ASCII.
//
// https://encoding.spec.whatwg.org/#security-background
if label == "utf-16le" {
return (vec![0x0A, 0x00], vec![0x0D, 0x00]);
}
if label == "utf-16be" {
return (vec![0x00, 0x0A], vec![0x00, 0x0D]);
}
// ASCII-compatible
(vec![b'\n'], vec![b'\r'])
}
#[tauri::command]
pub async fn read_text_file_lines_next<R: Runtime>(
webview: Webview<R>,
@@ -1203,22 +1242,39 @@ impl StdFileResource {
impl Resource for StdFileResource {}
/// Same as [std::io::Lines] but with bytes
struct LinesBytes<T: BufRead>(T);
struct LinesBytes<T: BufRead> {
bytes: T,
lf_bytes: Vec<u8>,
cr_bytes: Vec<u8>,
}
impl<T: BufRead> LinesBytes<T> {
fn new(bytes: T, lf_bytes: Vec<u8>, cr_bytes: Vec<u8>) -> Self {
LinesBytes {
bytes,
lf_bytes,
cr_bytes,
}
}
}
impl<B: BufRead> Iterator for LinesBytes<B> {
type Item = std::io::Result<Vec<u8>>;
fn next(&mut self) -> Option<std::io::Result<Vec<u8>>> {
let mut buf = Vec::new();
match self.0.read_until(b'\n', &mut buf) {
// Search for '\n'
match read_until_bytes(&mut self.bytes, &self.lf_bytes, &mut buf) {
Ok(0) => None,
Ok(_n) => {
if buf.last() == Some(&b'\n') {
buf.pop();
if buf.last() == Some(&b'\r') {
buf.pop();
// Remove '\n' or '\r\n'
if buf.ends_with(&self.lf_bytes) {
buf.truncate(buf.len() - self.lf_bytes.len());
if buf.ends_with(&self.cr_bytes) {
buf.truncate(buf.len() - self.cr_bytes.len());
}
}
Some(Ok(buf))
}
Err(e) => Some(Err(e)),
@@ -1226,11 +1282,35 @@ impl<B: BufRead> Iterator for LinesBytes<B> {
}
}
fn read_until_bytes(
r: &mut impl BufRead,
bytes: &[u8],
buf: &mut Vec<u8>,
) -> std::io::Result<usize> {
let last_byte = *bytes
.last()
.ok_or_else(|| std::io::Error::other("invalid empty bytes"))?;
if bytes.len() == 1 {
return r.read_until(last_byte, buf);
}
let mut total_n = 0;
loop {
let n = r.read_until(last_byte, buf)?;
total_n += n;
if n == 0 || buf.ends_with(bytes) {
return Ok(total_n);
}
}
}
struct StdLinesResource(Mutex<LinesBytes<BufReader<File>>>);
impl StdLinesResource {
fn new(lines: BufReader<File>) -> Self {
Self(Mutex::new(LinesBytes(lines)))
fn new(lines: BufReader<File>, lf_bytes: Vec<u8>, cr_bytes: Vec<u8>) -> Self {
Self(Mutex::new(LinesBytes::new(lines, lf_bytes, cr_bytes)))
}
fn with_lock<R, F: FnMut(&mut LinesBytes<BufReader<File>>) -> R>(&self, mut f: F) -> R {
@@ -1354,21 +1434,60 @@ mod test {
#[test]
fn test_lines_bytes() {
let base = String::from("line 1\nline2\nline 3\nline 4");
let bytes = base.as_bytes();
// UTF-8
{
let base = String::from("line 1\nline2\nline 3\r\nline 4");
let bytes = base.as_bytes();
let string1 = base.lines().collect::<String>();
let string2 = BufReader::new(bytes)
.lines()
.map_while(Result::ok)
.collect::<String>();
let string3 = LinesBytes(BufReader::new(bytes))
.flatten()
.flat_map(String::from_utf8)
.collect::<String>();
let string1 = base.lines().collect::<String>();
let string2 = BufReader::new(bytes)
.lines()
.map_while(Result::ok)
.collect::<String>();
let string3 = LinesBytes::new(BufReader::new(bytes), vec![b'\n'], vec![b'\r'])
.flatten()
.flat_map(String::from_utf8)
.collect::<String>();
assert_eq!(string1, string2);
assert_eq!(string1, string3);
assert_eq!(string2, string3);
assert_eq!(string1, string2);
assert_eq!(string1, string3);
assert_eq!(string2, string3);
}
// UTF-16 LE
{
fn utf16(text: &str) -> Vec<u8> {
text.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
}
let base = String::from("line 1\nline2\nline 3\r\nline 4\n");
let bytes = utf16(&base);
let mut lines = LinesBytes::new(BufReader::new(&bytes[..]), utf16("\n"), utf16("\r"));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 1")));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line2")));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 3")));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 4")));
assert!(lines.next().is_none());
}
// UTF-16 BE
{
fn utf16(text: &str) -> Vec<u8> {
text.encode_utf16().flat_map(|u| u.to_be_bytes()).collect()
}
// ਗ (U+0A17) encodes to 0x0A 0x17,
// which contains 0x0A but is not a line feed (U+000A = 0x00 0x0A).
let base = String::from("line 1\nline2ਗ\nline 3\r\nline 4");
let bytes = utf16(&base);
let mut lines = LinesBytes::new(BufReader::new(&bytes[..]), utf16("\n"), utf16("\r"));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 1")));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line2ਗ")));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 3")));
assert_eq!(lines.next().map(Result::unwrap), Some(utf16("line 4")));
assert!(lines.next().is_none());
}
}
}
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e){"use strict";async function t(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;const r="Request cancelled";return e.fetch=async function(e,n){const a=n?.signal;if(a?.aborted)throw new Error(r);const o=n?.maxRedirections,s=n?.connectTimeout,i=n?.proxy,d=n?.danger;n&&(delete n.maxRedirections,delete n.connectTimeout,delete n.proxy,delete n.danger);const c=n?.headers?n.headers instanceof Headers?n.headers:new Headers(n.headers):new Headers,u=new Request(e,n),l=await u.arrayBuffer(),_=0!==l.byteLength?Array.from(new Uint8Array(l)):null;for(const[e,t]of u.headers)c.get(e)||c.set(e,t);const h=(c instanceof Headers?Array.from(c.entries()):Array.isArray(c)?c:Object.entries(c)).map((([e,t])=>[e,"string"==typeof t?t:t.toString()]));if(a?.aborted)throw new Error(r);const f=await t("plugin:http|fetch",{clientConfig:{method:u.method,url:u.url,headers:h,data:_,maxRedirections:o,connectTimeout:s,proxy:i,danger:d}}),p=()=>t("plugin:http|fetch_cancel",{rid:f});if(a?.aborted)throw p(),new Error(r);a?.addEventListener("abort",(()=>{p()}));const{status:w,statusText:y,url:g,headers:b,rid:T}=await t("plugin:http|fetch_send",{rid:f}),R=()=>t("plugin:http|fetch_cancel_body",{rid:T}),m=[101,103,204,205,304].includes(w)?null:new ReadableStream({start:e=>{a?.addEventListener("abort",(()=>{e.error(r),R()}))},pull:e=>(async e=>{let r;try{r=await t("plugin:http|fetch_read_body",{rid:T})}catch(t){return e.error(t),void R()}const n=new Uint8Array(r),a=n[n.byteLength-1],o=n.slice(0,n.byteLength-1);1!==a?e.enqueue(o):e.close()})(e),cancel:()=>{R()}}),A=new Response(m,{status:w,statusText:y});return Object.defineProperty(A,"url",{value:g}),Object.defineProperty(A,"headers",{value:new Headers(b)}),A},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e){"use strict";async function t(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;const r="Request cancelled";return e.fetch=async function(e,n){const a=n?.signal;if(a?.aborted)throw new Error(r);const o=n?.maxRedirections,i=n?.connectTimeout,s=n?.proxy,d=n?.danger;n&&(delete n.maxRedirections,delete n.connectTimeout,delete n.proxy,delete n.danger);const c=n?.headers?n.headers instanceof Headers?n.headers:new Headers(n.headers):new Headers,l=new Request(e,n),u=await l.arrayBuffer(),f=0!==u.byteLength?Array.from(new Uint8Array(u)):null;for(const[e,t]of l.headers)c.get(e)||c.set(e,t);const _=(c instanceof Headers?Array.from(c.entries()):Array.isArray(c)?c:Object.entries(c)).map((([e,t])=>[e,"string"==typeof t?t:t.toString()]));if(a?.aborted)throw new Error(r);const h=await t("plugin:http|fetch",{clientConfig:{method:l.method,url:l.url,headers:_,data:f,maxRedirections:o,connectTimeout:i,proxy:s,danger:d}}),p=()=>t("plugin:http|fetch_cancel",{rid:h});if(a?.aborted)throw p(),new Error(r);a?.addEventListener("abort",(()=>{p()}));const{status:w,statusText:y,url:b,headers:g,rid:T}=await t("plugin:http|fetch_send",{rid:h}),R=()=>t("plugin:http|fetch_cancel_body",{rid:T}),m=[101,103,204,205,304].includes(w)?null:new ReadableStream({start:e=>{a?.addEventListener("abort",(()=>{e.error(r),R()}))},pull:e=>(async e=>{let r;try{r=await t("plugin:http|fetch_read_body",{rid:T})}catch(t){return e.error(t),void R()}const n=new Uint8Array(r),a=n[n.byteLength-1],o=n.slice(0,n.byteLength-1);1!==a?e.enqueue(o):e.close()})(e),cancel:()=>{R()}}),A=new Response(m,{status:w,statusText:y});Object.defineProperty(A,"url",{value:b,writable:!1}),Object.defineProperty(A,"headers",{value:new Headers(g),writable:!1});const v=A.clone.bind(A);return Object.defineProperty(A,"clone",{value:()=>{const e=v();return Object.defineProperty(e,"url",{value:b,writable:!1}),Object.defineProperty(e,"headers",{value:new Headers(g),writable:!1}),e}}),A},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})}
+23 -7
View File
@@ -287,14 +287,30 @@ export async function fetch(
statusText
})
// Set `Response` properties that are ignored by the
// constructor, like url and some headers
//
// Since url and headers are read only properties
// this is the only way to set them.
Object.defineProperty(res, 'url', { value: url })
// `Response.url` cannot be set via the constructor, so we define it manually
Object.defineProperty(res, 'url', { value: url, writable: false })
// Expose `set-cookie` via `response.headers` (and `getSetCookie()` where
// supported). This is not Fetch-spec compliant for network responses in
// browsers, where `set-cookie` is treated as a forbidden response
// header and is generally not readable from JavaScript.
Object.defineProperty(res, 'headers', {
value: new Headers(responseHeaders)
value: new Headers(responseHeaders),
writable: false
})
// Patch clone() per-instance so cloning preserves the overridden properties
const originalClone = res.clone.bind(res)
Object.defineProperty(res, 'clone', {
value: () => {
const cloned = originalClone()
Object.defineProperty(cloned, 'url', { value: url, writable: false })
Object.defineProperty(cloned, 'headers', {
value: new Headers(responseHeaders),
writable: false
})
return cloned
}
})
return res
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## \[2.4.0]
### Dependencies
- Upgraded to `deep-link@2.4.7`
### fix
- [`98e2c11e`](https://github.com/tauri-apps/plugins-workspace/commit/98e2c11eefc3ee562f1ed280efe7e8ea6ff0f3b0) ([#3194](https://github.com/tauri-apps/plugins-workspace/pull/3194) by [@mrquantumoff](https://github.com/tauri-apps/plugins-workspace/../../mrquantumoff)) **Breaking Change:** On Linux, the DBus ID/name will now be `<bundle-id>.SingleInstance` instead of `org.<bundle_id_underscores>.SingleInstance` to follow DBus specifications.
This will break the single-instance mechanism across different app versions if the app was installed multiple times.
Added `dbus_id` builder method, which can be used to restore previous behavior. For a bundle identifier of `com.tauri.my-example` this would be `dbus_id("org.com_tauri_my_example")`.
## \[2.3.7]
### Dependencies
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-single-instance"
version = "2.3.7"
version = "2.4.0"
description = "Ensure a single instance of your tauri app is running."
authors = { workspace = true }
license = { workspace = true }
@@ -22,7 +22,7 @@ serde_json = { workspace = true }
tauri = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
tauri-plugin-deep-link = { path = "../deep-link", version = "2.4.6", optional = true }
tauri-plugin-deep-link = { path = "../deep-link", version = "2.4.7", optional = true }
semver = { version = "1", optional = true }
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
@@ -35,13 +35,13 @@ pub fn init<R: Runtime>(
plugin::Builder::new("single-instance")
.setup(move |app, _api| {
let mut dbus_name = dbus_id.unwrap_or_else(|| app.config().identifier.clone());
dbus_name.push_str(".SingleInstance");
#[cfg(feature = "semver")]
{
dbus_name.push('_');
dbus_name.push_str(semver_compat_string(&app.package_info().version).as_str());
}
dbus_name.push_str(".SingleInstance");
let mut dbus_path = dbus_name.replace('.', "/").replace('-', "_");
if !dbus_path.starts_with('/') {
+1 -1
View File
@@ -189,7 +189,7 @@ importers:
specifier: ^2.10.1
version: 2.10.1
'@tauri-apps/plugin-deep-link':
specifier: 2.4.6
specifier: 2.4.7
version: link:../..
devDependencies:
'@tauri-apps/cli':