diff --git a/dotnet-bridge/Bridge.BinaryDispatch.cs b/dotnet-bridge/Bridge.BinaryDispatch.cs index d2a440b..7d96d9d 100644 --- a/dotnet-bridge/Bridge.BinaryDispatch.cs +++ b/dotnet-bridge/Bridge.BinaryDispatch.cs @@ -579,6 +579,12 @@ TypeCode.SByte or TypeCode.Byte or TypeCode.Int16 or TypeCode.UInt16 or { return original; } + // The C#/WinRT projection (FileOpenPicker, StorageFile, ...) when the app loads + // C#/WinRT, so the object casts to its projected type and works with + // WinRT.Interop.InitializeWithWindow. + var projected = TryProjectWinRT(nativePtr); + if (projected is not null && targetType.IsInstanceOfType(projected)) + return projected; // 1. Typed QI first: works for COM/CsWinRT interface types that carry a // [Guid] attribute. More precise than a generic RCW for strongly-typed // parameters such as Windows.UI.Xaml.UIElement. diff --git a/dotnet-bridge/Bridge.cs b/dotnet-bridge/Bridge.cs index 6133224..684debc 100644 --- a/dotnet-bridge/Bridge.cs +++ b/dotnet-bridge/Bridge.cs @@ -669,6 +669,30 @@ private static IntPtr ObtainNativePtr(object obj) } } + private static MethodInfo? s_marshalInspectableFromAbi; + + // WinRT.MarshalInspectable.FromAbi(ptr), found by reflection: the C#/WinRT projection + // of a native WinRT object. Null when C#/WinRT isn't loaded or can't project it. + private static object? TryProjectWinRT(IntPtr ptr) + { + try + { + if (s_marshalInspectableFromAbi is null) + { + var open = AppDomain.CurrentDomain.GetAssemblies() + .Select(a => a.GetType("WinRT.MarshalInspectable`1")) + .FirstOrDefault(t => t is not null); + s_marshalInspectableFromAbi = open?.MakeGenericType(typeof(object)).GetMethod( + "FromAbi", BindingFlags.Public | BindingFlags.Static, [typeof(IntPtr)]); + } + return s_marshalInspectableFromAbi?.Invoke(null, [ptr]); + } + catch + { + return null; + } + } + [UnmanagedCallersOnly(EntryPoint = "Invoke", CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] public static unsafe int Invoke( diff --git a/runtime/src/global_fns.rs b/runtime/src/global_fns.rs index b70e9ba..ded636b 100644 --- a/runtime/src/global_fns.rs +++ b/runtime/src/global_fns.rs @@ -3374,6 +3374,14 @@ fn bin_write_v8_arg( if arg.is_object() { if let Some(obj) = arg.to_object(scope) { + // A WinRT object: tag 0x0A + its IUnknown. Borrowed: the wrapper keeps it alive + // while the bridge wraps it, and the managed wrapper takes its own reference. + if let Some(instance) = crate::ns_proxy::this_instance(scope, obj) { + let ptr = instance.as_raw() as u64; + buf.push(0x0A); + buf.extend_from_slice(&ptr.to_le_bytes()); + return; + } // Dotnet bridge handle: { __handle: i32 } if let Some(key) = v8::String::new(scope, "__handle") { if let Some(hval) = obj.get(scope, key.into()) { @@ -3645,6 +3653,14 @@ fn bin_write_v8_value( if arg.is_object() { if let Some(obj) = arg.to_object(scope) { + // A WinRT object: tag 0x0A + its IUnknown. Borrowed: the wrapper keeps it alive + // while the bridge wraps it, and the managed wrapper takes its own reference. + if let Some(instance) = crate::ns_proxy::this_instance(scope, obj) { + let ptr = instance.as_raw() as u64; + buf.push(0x0A); + buf.extend_from_slice(&ptr.to_le_bytes()); + return; + } // Dotnet bridge handle: { __handle: i32 } if let Some(key) = v8::String::new(scope, "__handle") { if let Some(hval) = obj.get(scope, key.into()) { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 1b1673f..1faa462 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -139,15 +139,12 @@ use windows::core::{IUnknown, Interface, GUID, HSTRING, PCWSTR}; use windows::core::{Error, HRESULT}; #[cfg(feature = "classic")] use windows::core::IInspectable; -#[cfg(feature = "classic")] -use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::Diagnostics::Debug::OutputDebugStringW; use windows::Win32::System::WinRT::RoGetActivationFactory; #[cfg(feature = "classic")] use windows::Win32::System::WinRT::{ IActivationFactory, RoInitialize, RoUninitialize, RO_INIT_SINGLETHREADED, }; -#[cfg(feature = "classic")] use windows::Win32::UI::Shell::IInitializeWithWindow; use windows::Win32::UI::WindowsAndMessaging::{ DispatchMessageW, PeekMessageW, TranslateMessage, MSG, PM_REMOVE, @@ -870,27 +867,32 @@ fn fill_class_members( } } }; - let mut absorb_methods = |list: &[MethodDeclaration]| { - for m in list { - let key = if !m.overload_name().is_empty() { - m.overload_name() - } else { - m.name() - }; - if !methods.contains_key(key) { - methods.insert(key.to_string(), m.clone()); - } - } - }; + let mut lists: Vec<&[MethodDeclaration]> = vec![class_declaration.methods()]; absorb_props(class_declaration.properties()); - absorb_methods(class_declaration.methods()); if let Some(di) = class_declaration.default_interface() { absorb_props(di.properties()); - absorb_methods(di.methods()); + lists.push(di.methods()); } for iface in class_declaration.implemented_interfaces() { absorb_props(iface.properties()); - absorb_methods(iface.methods()); + lists.push(iface.methods()); + } + for m in lists.iter().flat_map(|l| l.iter()) { + let key = if !m.overload_name().is_empty() { + m.overload_name() + } else { + m.name() + }; + if !methods.contains_key(key) { + methods.insert(key.to_string(), m.clone()); + } + } + // The public name must reach overloads that all carry an `[Overload]` name + // (`StorageFile.CopyAsync` is `CopyOverload*`); callers switch arity from there. + for m in lists.iter().flat_map(|l| l.iter()) { + if !methods.contains_key(m.name()) { + methods.insert(m.name().to_string(), m.clone()); + } } if !class_declaration.base_full_name().is_empty() { if let Some(base_decl) = MetadataReader::find_by_name(class_declaration.base_full_name()) { @@ -1186,10 +1188,34 @@ pub(crate) fn return_value_from_kind<'a>( ReturnKind::Struct(declaration) => { create_struct_object_from_raw(declaration.clone(), result, scope).into() } + ReturnKind::InterfaceObject { decl, type_name } => { + if result.is_null() { + v8::null(scope).into() + } else { + let instance = unsafe { IUnknown::from_raw(result) }; + // Wrap by the runtime class: an interface such as IPropertySet declares no + // members itself and only requires the generic IMap/IIterable instances. + match crate::ns_proxy::resolve_runtime_class(&instance, type_name.as_ref()) { + Some((name, class)) => { + let instance = Some(instance); + create_ns_ctor_instance_object(&name, None, None, class, instance, scope) + .into() + } + None => create_ns_ctor_instance_object( + type_name.as_ref(), + None, + parent, + decl.clone(), + Some(instance), + scope, + ) + .into(), + } + } + } ReturnKind::Object { decl, type_name, .. - } - | ReturnKind::InterfaceObject { decl, type_name } => { + } => { if result.is_null() { v8::null(scope).into() } else { @@ -1733,18 +1759,60 @@ pub(crate) fn throw_js_error(scope: &mut v8::PinScope<'_, '_>, message: &str) { } } -/// `IInitializeWithWindow` support probe for a freshly constructed object. A raw QueryInterface: -/// `cast` builds a `windows_core::Error` (which calls GetErrorInfo) for the E_NOINTERFACE -/// outcome that almost every class produces, on every construct. -#[cfg(feature = "classic")] -fn query_initialize_with_window(unk: &IUnknown) -> Option { +/// Gives a freshly constructed object that supports `IInitializeWithWindow` (pickers, dialogs) +/// the app's window as its owner. A raw QueryInterface: `cast` builds a `windows_core::Error` +/// (which calls GetErrorInfo) for the E_NOINTERFACE outcome that almost every class produces, +/// on every construct. +pub(crate) fn initialize_with_owner_window(unk: &IUnknown) { let mut out: *mut c_void = std::ptr::null_mut(); let hr = unsafe { unk.query(&IInitializeWithWindow::IID, &mut out) }; - if hr.is_ok() && !out.is_null() { - Some(unsafe { IInitializeWithWindow::from_raw(out) }) - } else { - None + if hr.is_err() || out.is_null() { + return; + } + let init = unsafe { IInitializeWithWindow::from_raw(out) }; + if let Some(hwnd) = owner_window() { + let _ = unsafe { init.Initialize(hwnd) }; + } +} + +/// A visible, unowned top-level window of this thread, else of this process, else the console. +fn owner_window() -> Option { + use windows::Win32::Foundation::{HWND, LPARAM}; + use windows::Win32::System::Threading::{GetCurrentProcessId, GetCurrentThreadId}; + use windows::Win32::UI::WindowsAndMessaging::{ + EnumThreadWindows, EnumWindows, GetWindow, GetWindowThreadProcessId, IsWindowVisible, + GW_OWNER, + }; + + unsafe extern "system" fn pick(hwnd: HWND, found: LPARAM) -> windows::core::BOOL { + let unowned = GetWindow(hwnd, GW_OWNER).map_or(true, |owner| owner.is_invalid()); + if IsWindowVisible(hwnd).as_bool() && unowned { + *(found.0 as *mut HWND) = hwnd; + return false.into(); + } + true.into() + } + unsafe extern "system" fn pick_in_process(hwnd: HWND, found: LPARAM) -> windows::core::BOOL { + let mut pid = 0u32; + GetWindowThreadProcessId(hwnd, Some(&mut pid)); + if pid != GetCurrentProcessId() { + return true.into(); + } + pick(hwnd, found) + } + + let mut found = HWND::default(); + let slot = LPARAM(&mut found as *mut HWND as isize); + unsafe { + let _ = EnumThreadWindows(GetCurrentThreadId(), Some(pick), slot); + if found.is_invalid() { + let _ = EnumWindows(Some(pick_in_process), slot); + } + if found.is_invalid() { + found = windows::Win32::System::Console::GetConsoleWindow(); + } } + (!found.is_invalid()).then_some(found) } pub(crate) fn class_activation_factory(full_name: &str) -> windows::core::Result { @@ -6196,12 +6264,7 @@ fn create_ns_ctor_object<'a>( } }; - if let Some(init) = query_initialize_with_window(&result) { - let hwnd = unsafe { GetConsoleWindow() }; - if !hwnd.is_invalid() { - let _ = unsafe { init.Initialize(hwnd) }; - } - } + crate::initialize_with_owner_window(&result); { let declaration = Arc::clone(&class_declaration); @@ -6259,12 +6322,7 @@ fn create_ns_ctor_object<'a>( } let result = unsafe { IUnknown::from_raw(qi_ptr) }; - if let Some(init) = query_initialize_with_window(&result) { - let hwnd = unsafe { GetConsoleWindow() }; - if !hwnd.is_invalid() { - let _ = unsafe { init.Initialize(hwnd) }; - } - } + crate::initialize_with_owner_window(&result); { let declaration = Arc::clone(&class_declaration); @@ -9089,6 +9147,9 @@ mod instance_cache_test; #[cfg(all(test, feature = "classic"))] mod interop_test; +#[cfg(all(test, feature = "classic"))] +mod projection_test; + #[cfg(all(test, feature = "classic"))] mod js_delegate_tests { use super::{ diff --git a/runtime/src/napi_engine/dotnet.rs b/runtime/src/napi_engine/dotnet.rs index ee40f57..eec07cd 100644 --- a/runtime/src/napi_engine/dotnet.rs +++ b/runtime/src/napi_engine/dotnet.rs @@ -98,6 +98,17 @@ unsafe fn addref_native_ptr(ptr: u64) { ((*vtable).AddRef)(unknown.as_raw()); } +/// The COM instance of a WinRT wrapper (host object or proxy), borrowed: the wrapper keeps it +/// alive while the bridge wraps it, and the managed wrapper takes its own reference. +fn winrt_instance_ptr(env: &Env, obj: &JsObject) -> Option { + if let Ok(dec) = env.unwrap::(obj) { + return dec.instance.as_ref().map(|i| i.as_raw() as u64); + } + let handle = obj.get_named_property::("handle").ok()?; + let ptr = crate::napi_engine::value::ptr_from_external(env, &handle)?; + (!ptr.is_null()).then_some(ptr as u64) +} + /// Pins `func` as a callback the bridge can invoke; returns its id. fn pin_js_callback(env: &Env, func: sys::napi_value) -> Option { let mut func_ref: sys::napi_ref = std::ptr::null_mut(); @@ -371,6 +382,12 @@ fn napi_bin_write_arg(env: &Env, buf: &mut Vec, arg: &JsUnknown) { return; } } + // A WinRT object: tag 0x0A + its IUnknown (borrowed, see `winrt_instance_ptr`). + if let Some(ptr) = winrt_instance_ptr(env, &obj) { + buf.push(0x0A); + buf.extend_from_slice(&ptr.to_le_bytes()); + return; + } if let Ok(pval) = obj.get_named_property::("__native_ptr") { if let Ok(ValueType::BigInt) = pval.get_type() { let b: JsBigInt = unsafe { pval.cast() }; @@ -456,6 +473,12 @@ fn napi_bin_write_value(env: &Env, buf: &mut Vec, arg: &JsUnknown) { return; } } + // A WinRT object: tag 0x0A + its IUnknown (borrowed, see `winrt_instance_ptr`). + if let Some(ptr) = winrt_instance_ptr(env, &obj) { + buf.push(0x0A); + buf.extend_from_slice(&ptr.to_le_bytes()); + return; + } if let Ok(pval) = obj.get_named_property::("__native_ptr") { if let Ok(ValueType::BigInt) = pval.get_type() { let b: JsBigInt = unsafe { pval.cast() }; diff --git a/runtime/src/napi_engine/ns_hostobject.rs b/runtime/src/napi_engine/ns_hostobject.rs index 542ce1f..fd892eb 100644 --- a/runtime/src/napi_engine/ns_hostobject.rs +++ b/runtime/src/napi_engine/ns_hostobject.rs @@ -165,15 +165,24 @@ pub fn is_interface(declaration: &Decl) -> bool { ) } +/// The metadata class named by `instance`'s GetRuntimeClassName. +pub fn runtime_class(instance: &IUnknown) -> Option<(String, Decl)> { + let inspectable = instance.cast::().ok()?; + let name = unsafe { inspectable.GetRuntimeClassName() }.ok()?.to_string(); + let declaration = MetadataReader::find_by_name(&name)?; + if !matches!(declaration.read().kind(), DeclarationKind::Class) { + return None; + } + Some((name, declaration)) +} + /// For an interface-typed instance, ask the concrete object for its runtime class and, if that class /// is host-eligible, return its (name, declaration) so it can be host-wrapped instead of Proxy'd. pub fn runtime_class_host_target(instance: &IUnknown) -> Option<(String, Decl)> { if !host_enabled() { return None; } - let inspectable = instance.cast::().ok()?; - let name = unsafe { inspectable.GetRuntimeClassName() }.ok()?.to_string(); - let declaration = MetadataReader::find_by_name(&name)?; + let (name, declaration) = runtime_class(instance)?; let eligible = { let lock = declaration.read(); match lock.as_any().downcast_ref::() { @@ -324,9 +333,11 @@ fn activate_prepared( }; let inspectable: IInspectable = unsafe { af.ActivateInstance() } .map_err(|e| generic_error(format!("ActivateInstance failed for {class_name}: {e}")))?; - inspectable + let instance = inspectable .cast::() - .map_err(|e| generic_error(format!("activated-instance cast failed: {e}"))) + .map_err(|e| generic_error(format!("activated-instance cast failed: {e}")))?; + crate::initialize_with_owner_window(&instance); + Ok(instance) } /// Parameterized construction (`new Class(args)`): pick the initializer by arity, prepare it on diff --git a/runtime/src/napi_engine/ns_proxy.rs b/runtime/src/napi_engine/ns_proxy.rs index 1ae8740..6b54987 100644 --- a/runtime/src/napi_engine/ns_proxy.rs +++ b/runtime/src/napi_engine/ns_proxy.rs @@ -947,9 +947,11 @@ pub(crate) fn activate_instance(class_name: &str) -> Result .map_err(|e| generic_error(format!("{class_name} is not default-activatable: {e}")))?; let inspectable: IInspectable = unsafe { af.ActivateInstance() } .map_err(|e| generic_error(format!("ActivateInstance failed for {class_name}: {e}")))?; - inspectable + let instance = inspectable .cast::() - .map_err(|e| generic_error(format!("activated-instance cast failed: {e}"))) + .map_err(|e| generic_error(format!("activated-instance cast failed: {e}")))?; + crate::initialize_with_owner_window(&instance); + Ok(instance) } /// Backing state for an instance proxy, shared by its traps (Rc'd into each closure). @@ -1230,6 +1232,20 @@ pub fn create_instance_proxy( return Ok(obj); } + // An interface such as IPropertySet declares no members itself (only the generic IMap and + // IIterable it requires), so proxy the runtime class when metadata has it. + let resolved = if !class_name.contains('<') + && crate::napi_engine::ns_hostobject::is_interface(&declaration) + { + crate::napi_engine::ns_hostobject::runtime_class(&instance) + } else { + None + }; + let (class_name, declaration) = match &resolved { + Some((name, decl)) => (name.as_str(), decl.clone()), + None => (class_name, declaration), + }; + // Proxy path (interfaces, collections, or host disabled). Identity by the declared type. if let Some(id) = identity { if let Some(existing) = cached_instance(env, id, class_name) { diff --git a/runtime/src/ns_proxy.rs b/runtime/src/ns_proxy.rs index d694a1c..c187f01 100644 --- a/runtime/src/ns_proxy.rs +++ b/runtime/src/ns_proxy.rs @@ -39,9 +39,7 @@ use v8::{FunctionTemplate, Local}; use windows::core::{IInspectable, IUnknown, Interface, HSTRING}; use windows::Foundation::Collections::IPropertySet; use windows::Foundation::PropertyValue; -use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::WinRT::IActivationFactory; -use windows::Win32::UI::Shell::IInitializeWithWindow; // Track constructors currently being built on this thread to avoid re-entrant // template/property mutations that can corrupt V8 descriptor arrays. @@ -2018,6 +2016,25 @@ pub(crate) fn create_ns_object<'a>( object.into() } +/// The metadata class named by `instance`'s GetRuntimeClassName, when it differs from +/// `declared` and neither name is a generic instance. +pub(crate) fn resolve_runtime_class( + instance: &IUnknown, + declared: &str, +) -> Option<(String, Arc>)> { + if declared.contains('<') { + return None; + } + instance + .cast::() + .ok() + .and_then(|insp| insp.GetRuntimeClassName().ok()) + .map(|cn| cn.to_string()) + .filter(|cn| !cn.is_empty() && cn != declared && !cn.contains('<')) + .and_then(|cn| MetadataReader::find_by_name(&cn).map(|d| (cn, d))) + .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)) +} + pub(crate) fn create_ns_ctor_instance_object<'a>( name: &str, factory: Option, @@ -2035,18 +2052,9 @@ pub(crate) fn create_ns_ctor_instance_object<'a>( } } - let resolved_concrete: Option<(String, Arc>)> = if name.contains('<') { - None - } else { - instance - .as_ref() - .and_then(|unk| unk.cast::().ok()) - .and_then(|insp| insp.GetRuntimeClassName().ok()) - .map(|cn| cn.to_string()) - .filter(|cn| !cn.is_empty() && cn != name && !cn.contains('<')) - .and_then(|cn| MetadataReader::find_by_name(&cn).map(|d| (cn, d))) - .filter(|(_, d)| !matches!(d.read().kind(), DeclarationKind::Struct)) - }; + let resolved_concrete = instance + .as_ref() + .and_then(|unk| resolve_runtime_class(unk, name)); let (name, declaration): (&str, Arc>) = match &resolved_concrete { Some((cn, d)) => (cn.as_str(), d.clone()), None => (name, declaration), @@ -2119,6 +2127,8 @@ pub(crate) fn create_ns_ctor_instance_object<'a>( } else { method.overload_name().to_string() }; + // The prototype binds one overload per name; the callback switches on argc. + crate::class_helpers::register_overload_siblings(clazz, &method_name, method); let is_static = method.is_static(); let key = format!( "{}:{}", @@ -2154,6 +2164,9 @@ pub(crate) fn create_ns_ctor_instance_object<'a>( let dec = unsafe { &*dec }; let lock = dec.read(); let method = lock.as_any().downcast_ref::().unwrap(); + let argc = args.length() as usize; + let alt = crate::class_helpers::overload_for_argc(method, argc); + let method = alt.as_ref().unwrap_or(method); let Some(mut method) = receiver_method_call(scope, args.this(), dec, method, method.is_sealed()) else { return; }; @@ -3306,12 +3319,7 @@ pub(crate) fn create_ns_ctor_object<'a>( // when XAML later calls UIElement vtable slots on it. let result: IUnknown = instance.into(); - if let Ok(init) = result.cast::() { - let hwnd = unsafe { GetConsoleWindow() }; - if !hwnd.is_invalid() { - let _ = unsafe { init.Initialize(hwnd) }; - } - } + crate::initialize_with_owner_window(&result); let instance = create_ns_ctor_instance_object( clazz.name(), @@ -3363,12 +3371,7 @@ pub(crate) fn create_ns_ctor_object<'a>( // when XAML calls UIElement vtable slots on the stored object. let result = IUnknown::from_raw(result); - if let Ok(init) = result.cast::() { - let hwnd = unsafe { GetConsoleWindow() }; - if !hwnd.is_invalid() { - let _ = unsafe { init.Initialize(hwnd) }; - } - } + crate::initialize_with_owner_window(&result); let instance = create_ns_ctor_instance_object( clazz.name(), diff --git a/runtime/src/projection_test.rs b/runtime/src/projection_test.rs new file mode 100644 index 0000000..7c3744f --- /dev/null +++ b/runtime/src/projection_test.rs @@ -0,0 +1,68 @@ +use crate::Runtime; + +fn run(name: &str, script: &str) { + let name = name.to_string(); + let temp = std::env::temp_dir().to_string_lossy().trim_end_matches('\\').to_string(); + let script = script.replace("__TEMP_DIR__", &format!("{temp:?}")); + std::thread::spawn(move || { + let mut runtime = Runtime::new("."); + runtime.run_script(&script, &format!("{name}.js")); + if let Some(err) = crate::get_last_js_error() { + panic!("JS error in {name}: {err}"); + } + }) + .join() + .unwrap(); +} + +/// `CoreApplication.Properties` is declared as `IPropertySet`, which has no members of its own; +/// the wrapper must come from the runtime class so the IMap surface is callable. +#[test] +fn test_interface_typed_return_wraps_runtime_class() { + run( + "property_set_return", + r#" + const props = Windows.ApplicationModel.Core.CoreApplication.Properties; + if (typeof props.HasKey !== 'function') throw new Error('HasKey missing on ' + String(props)); + props.Insert('ns-test', 'všŸ˜€'); + if (!props.HasKey('ns-test')) throw new Error('HasKey after Insert'); + if (props.Lookup('ns-test') !== 'všŸ˜€') throw new Error('Lookup ' + props.Lookup('ns-test')); + props.Remove('ns-test'); + if (props.HasKey('ns-test')) throw new Error('HasKey after Remove'); + "#, + ); +} + +/// `StorageFile.CopyAsync`/`MoveAsync` overloads all carry `[Overload]` names (`CopyOverload*`); +/// the public name must still be callable and dispatch on arity, as must `RenameAsync(name)` and +/// `DeleteAsync()`, whose public names bind to the overload with an extra option argument. +#[test] +fn test_public_name_reaches_overload_named_methods() { + run( + "storage_overloads", + r#" + const S = Windows.Storage; + function wait(op) { + NSWinRT.wait(op, { timeoutMs: 10000 }); + if (Number(op.Status) !== 1) throw new Error('op failed: ' + JSON.stringify(op.ErrorCode)); + return op.GetResults(); + } + const temp = wait(S.StorageFolder.GetFolderFromPathAsync(__TEMP_DIR__)); + const dir = wait(temp.CreateFolderAsync('ns-overloads-' + Date.now(), S.CreationCollisionOption.GenerateUniqueName)); + try { + const sub = wait(dir.CreateFolderAsync('sub')); + const file = wait(dir.CreateFileAsync('a.txt')); + const copy = wait(file.CopyAsync(dir, 'b.txt', S.NameCollisionOption.FailIfExists)); + if (copy.Name !== 'b.txt') throw new Error('CopyAsync(3) → ' + copy.Name); + wait(copy.MoveAsync(sub)); + if (copy.Path !== sub.Path + '\\b.txt') throw new Error('MoveAsync(1) → ' + copy.Path); + wait(file.RenameAsync('c.txt')); + if (file.Name !== 'c.txt') throw new Error('RenameAsync(1) → ' + file.Name); + wait(file.DeleteAsync()); + if (wait(dir.TryGetItemAsync('c.txt')) !== null) throw new Error('DeleteAsync() left the file'); + } finally { + wait(dir.DeleteAsync(S.StorageDeleteOption.PermanentDelete)); + } + "#, + ); +}