Files
oak-editor/crates/oaktask/tests/project_task_test.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

616 lines
20 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Project task factory tests (project.rs + ffi/project.rs): load, save,
//! import, OTIO load/save and pre-cache/export factories.
mod common;
use common::*;
use std::ffi::{c_char, c_int, c_void};
use std::sync::atomic::Ordering;
use oaktask::error::{OAKTASK_E_INVALID, OAKTASK_E_NOT_FOUND};
use oaktask::ffi::manager::{oaktask_manager_init, oaktask_manager_shutdown};
use oaktask::ffi::project::{
oaktask_create_export, oaktask_create_precache, oaktask_create_project_import,
oaktask_create_project_load, oaktask_create_project_load_otio, oaktask_create_project_save,
oaktask_create_project_save_otio, oaktask_import_footage_at, oaktask_import_footage_count,
oaktask_import_invalid_at, oaktask_import_invalid_count,
oaktask_import_set_image_sequence_confirm_cb, oaktask_import_take_command,
oaktask_load_otio_set_confirm_cb, oaktask_load_otio_take_project, oaktask_load_take_project,
};
use oaktask::ffi::task::{
oaktask_debug_alive_count, oaktask_task_error, oaktask_task_free, oaktask_task_is_finished,
oaktask_task_start_sync, oaktask_task_title,
};
use oaktask::handle::CHandle;
unsafe extern "C" fn accept_all(
_seq: *const *const c_char,
_count: c_int,
_ud: *mut c_void,
) -> c_int {
1
}
unsafe extern "C" fn reject_all(
_seq: *const *const c_char,
_count: c_int,
_ud: *mut c_void,
) -> c_int {
0
}
unsafe extern "C" fn image_seq_confirm(_filename: *const c_char, _ud: *mut c_void) -> c_int {
1
}
fn free(t: &mut CHandle) {
unsafe {
oaktask_task_free(t);
}
}
/// Given a valid project file, `create_project_load` returns a task whose
/// `start_sync` succeeds and whose `load_take_project` transfers an
/// initialized project with a root folder.
#[test]
fn project_load_transfers_initialized_project() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
let path = std::env::temp_dir().join("oak-load-ok.oakproj");
std::fs::write(&path, b"oakproj-stub").unwrap();
let path_str = path.to_string_lossy().into_owned();
let mut task = unsafe { oaktask_create_project_load(common::cstr_of(&path_str)) };
assert!(!task.ctx.is_null());
// Title embeds the filename.
let needed = unsafe { oaktask_task_title(task, std::ptr::null_mut(), 0) };
assert!(needed > 10);
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1);
// take_project transfers a non-empty project.
let project = unsafe { oaktask_load_take_project(task) };
assert!(!project.ctx.is_null(), "load_take_project returned empty");
// Second take is empty (ownership transferred).
let project2 = unsafe { oaktask_load_take_project(task) };
assert!(project2.ctx.is_null());
free(&mut task);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
let _ = std::fs::remove_file(&path);
}
/// Loading a missing file fails: start_sync=0, an error is set, and
/// take_project is empty.
#[test]
fn project_load_missing_file_fails() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
let missing = "/tmp/oak-does-not-exist-12345.oakproj";
let _ = std::fs::remove_file(missing);
let mut task = unsafe { oaktask_create_project_load(common::cstr_of(missing)) };
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0);
assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1);
let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) };
assert!(needed > 0);
let mut buf = vec![0i8; needed as usize];
unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) };
assert!(cstr_read(&buf).contains("Failed to read file"));
let project = unsafe { oaktask_load_take_project(task) };
assert!(project.ctx.is_null());
// Null filename -> empty handle (factory failure path).
let empty = unsafe { oaktask_create_project_load(std::ptr::null()) };
assert!(empty.ctx.is_null());
free(&mut task);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// Given a loaded project, `create_project_save` writes the project and the
/// task succeeds with compression on and off.
#[test]
fn project_save_writes_with_and_without_compression() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
let path = std::env::temp_dir().join("oak-save-compressed.oakproj");
let path_str = path.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&path);
// Without compression.
let mut task =
unsafe { oaktask_create_project_save(fake_handle(), common::cstr_of(&path_str), 0) };
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert!(path.exists(), "saved file was not written");
free(&mut task);
// With compression (overwrites).
let _ = std::fs::remove_file(&path);
let mut task =
unsafe { oaktask_create_project_save(fake_handle(), common::cstr_of(&path_str), 1) };
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert!(path.exists());
free(&mut task);
// Null project -> empty handle.
let empty =
unsafe { oaktask_create_project_save(CHandle::null(), common::cstr_of(&path_str), 0) };
assert!(empty.ctx.is_null());
let _ = std::fs::remove_file(&path);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// Given an import folder and a media URL, `create_project_import` runs to
/// completion, records one footage and exposes it via `import_footage_at`.
#[test]
fn project_import_records_and_exposes_footage() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
let file = std::env::temp_dir().join("oak-import-ok.mp4");
std::fs::write(&file, b"media").unwrap();
let file_str = file.to_string_lossy().into_owned();
let urls = [common::cstr_of(&file_str)];
let mut task = unsafe {
oaktask_create_project_import(
fake_handle(),
fake_handle(),
urls.as_ptr(),
urls.len() as c_int,
)
};
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert_eq!(unsafe { oaktask_import_footage_count(task) }, 1);
assert_eq!(unsafe { oaktask_import_invalid_count(task) }, 0);
// footage_at returns an addref'd handle for in-range indices.
let footage = unsafe { oaktask_import_footage_at(task, 0) };
assert!(!footage.ctx.is_null());
// Out of range -> empty.
assert!(unsafe { oaktask_import_footage_at(task, 5) }.ctx.is_null());
assert!(unsafe { oaktask_import_footage_at(task, -1) }.ctx.is_null());
// The undo command is produced; redo adds the folder child, undo
// removes it again.
let mut command = unsafe { oaktask_import_take_command(task) };
assert!(!command.ctx.is_null());
// Second take is empty.
assert!(unsafe { oaktask_import_take_command(task) }.ctx.is_null());
assert_eq!(
unsafe { oaktask::bridge::undo::oakundo_command_redo_now(command) },
0
);
assert_eq!(
unsafe { oaktask::bridge::node::oaknode_folder_child_count(fake_handle()) },
1
);
assert_eq!(
unsafe { oaktask::bridge::undo::oakundo_command_undo_now(command) },
0
);
assert_eq!(
unsafe { oaktask::bridge::node::oaknode_folder_child_count(fake_handle()) },
0
);
unsafe {
oaktask::bridge::undo::oakundo_command_free(&mut command);
}
free(&mut task);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
let _ = std::fs::remove_file(&file);
}
/// Given an unreadable import URL, `create_project_import` records the
/// invalid filename and `import_invalid_at` reports it (two-stage).
#[test]
fn project_import_reports_invalid_urls() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
FOOTAGE_VALID.store(0, Ordering::SeqCst);
let bad = "/tmp/oak-invalid-media-12345.mp4";
let _ = std::fs::remove_file(bad);
let urls = [common::cstr_of(bad)];
let mut task = unsafe {
oaktask_create_project_import(
fake_handle(),
fake_handle(),
urls.as_ptr(),
urls.len() as c_int,
)
};
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert_eq!(unsafe { oaktask_import_invalid_count(task) }, 1);
assert_eq!(unsafe { oaktask_import_footage_count(task) }, 0);
// Two-stage invalid filename getter.
let needed = unsafe { oaktask_import_invalid_at(task, 0, std::ptr::null_mut(), 0) };
assert!(needed > 0);
let mut buf = vec![0i8; needed as usize];
let r = unsafe { oaktask_import_invalid_at(task, 0, buf.as_mut_ptr(), needed) };
assert_eq!(r, needed);
assert_eq!(cstr_read(&buf), bad);
// Out of range -> E_NOT_FOUND.
assert_eq!(
unsafe { oaktask_import_invalid_at(task, 3, std::ptr::null_mut(), 0) },
OAKTASK_E_NOT_FOUND
);
// Factory failure paths: null folder, null project, negative count.
assert!(unsafe {
oaktask_create_project_import(CHandle::null(), fake_handle(), std::ptr::null(), 0)
}
.ctx
.is_null());
assert!(unsafe {
oaktask_create_project_import(fake_handle(), CHandle::null(), std::ptr::null(), 0)
}
.ctx
.is_null());
let urls_null = [std::ptr::null()];
assert!(unsafe {
oaktask_create_project_import(fake_handle(), fake_handle(), urls_null.as_ptr(), 1)
}
.ctx
.is_null());
// Accessors on a non-import task -> E_INVALID / empty.
let mut save_task =
unsafe { oaktask_create_project_save(fake_handle(), common::cstr_of("/tmp/x.oakproj"), 0) };
assert_eq!(
unsafe { oaktask_import_footage_count(save_task) },
OAKTASK_E_INVALID
);
assert!(unsafe { oaktask_import_take_command(save_task) }
.ctx
.is_null());
free(&mut save_task);
free(&mut task);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// An image sequence with a confirmation callback imports once and prunes
/// the remaining frames from the list.
#[test]
fn project_import_image_sequence_confirmed() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
let dir = std::env::temp_dir().join("oak-seq");
let _ = std::fs::create_dir_all(&dir);
let files = ["seq001.png", "seq002.png", "seq003.png"];
let mut paths = Vec::new();
for f in &files {
let p = dir.join(f);
std::fs::write(&p, b"png").unwrap();
paths.push(p.to_string_lossy().into_owned());
}
IMAGE_SEQUENCE_DIGIT_COUNT.store(3, Ordering::SeqCst);
CONFIG_DEFAULT_SEQ_FRAME_RATE.store(1, Ordering::SeqCst);
unsafe {
oaktask_import_set_image_sequence_confirm_cb(Some(image_seq_confirm), std::ptr::null_mut())
};
let urls: Vec<*const c_char> = paths.iter().map(|p| common::cstr_of(p)).collect();
let mut task = unsafe {
oaktask_create_project_import(
fake_handle(),
fake_handle(),
urls.as_ptr(),
urls.len() as c_int,
)
};
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
// The three frames collapse into a single footage import.
assert_eq!(unsafe { oaktask_import_footage_count(task) }, 1);
assert_eq!(unsafe { oaktask_import_invalid_count(task) }, 0);
unsafe { oaktask_import_set_image_sequence_confirm_cb(None, std::ptr::null_mut()) };
IMAGE_SEQUENCE_DIGIT_COUNT.store(0, Ordering::SeqCst);
free(&mut task);
for p in &paths {
let _ = std::fs::remove_file(p);
}
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// Given a valid OTIO file, `create_project_load_otio` runs to completion and
/// `load_otio_take_project` transfers an initialized project; a missing file
/// fails cleanly. (OTIO parsing now happens inside the task through the
/// `oakotio` binding — README decision #6.)
#[test]
fn otio_load_transfers_project_with_default_confirm() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
// A minimal Timeline document (no tracks).
let path = std::env::temp_dir().join("oak-load-ok.otio");
let otio = r#"{
"OTIO_SCHEMA": "Timeline.1",
"metadata": {},
"name": "T",
"global_start_time": null,
"tracks": {
"OTIO_SCHEMA": "Stack.1",
"metadata": {},
"name": "tracks",
"source_range": null,
"effects": [],
"markers": [],
"enabled": true,
"children": []
}
}"#;
std::fs::write(&path, otio).unwrap();
let path_str = path.to_string_lossy().into_owned();
let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&path_str)) };
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1);
assert!(!(unsafe { oaktask_load_otio_take_project(task) })
.ctx
.is_null());
free(&mut task);
// Missing file -> failure with a clear error.
let missing = std::env::temp_dir().join("oak-missing-otio.otio");
let missing_str = missing.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&missing);
let mut task = unsafe { oaktask_create_project_load_otio(common::cstr_of(&missing_str)) };
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0);
let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) };
assert!(needed > 0);
let mut buf = vec![0i8; needed as usize];
unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) };
assert!(
cstr_read(&buf).contains("Failed to load OpenTimelineIO"),
"error was: {}",
cstr_read(&buf)
);
assert!(unsafe { oaktask_load_otio_take_project(task) }
.ctx
.is_null());
// The confirm callback setters are no-ops (null clears).
unsafe {
oaktask_load_otio_set_confirm_cb(Some(accept_all), std::ptr::null_mut());
oaktask_load_otio_set_confirm_cb(Some(reject_all), std::ptr::null_mut());
oaktask_load_otio_set_confirm_cb(None, std::ptr::null_mut());
}
free(&mut task);
let _ = std::fs::remove_file(&path);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// Given a project with a sequence, `create_project_save_otio` writes the
/// OTIO file and the task succeeds; a project without sequences fails.
#[test]
fn otio_save_writes_file() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
// One sequence in the root folder, one video track with a single clip.
FOLDER_CHILD_COUNT.store(1, Ordering::SeqCst);
*NODE_ID.lock().unwrap() = "org.olivevideoeditor.Olive.sequence".to_string();
*NODE_LABEL.lock().unwrap() = "My Sequence".to_string();
VIDEO_FRAME_RATE_NUM.store(25, Ordering::SeqCst);
VIDEO_FRAME_RATE_DEN.store(1, Ordering::SeqCst);
TRACK_LIST_TRACK_COUNT.store(1, Ordering::SeqCst);
TRACK_BLOCK_COUNT.store(1, Ordering::SeqCst);
BLOCK_KIND.store(1, Ordering::SeqCst); // OAKNODE_BLOCK_CLIP
BLOCK_IN_NUM.store(0, Ordering::SeqCst);
BLOCK_IN_DEN.store(1, Ordering::SeqCst);
BLOCK_LENGTH_NUM.store(25, Ordering::SeqCst);
BLOCK_LENGTH_DEN.store(1, Ordering::SeqCst);
*FOOTAGE_FILENAME.lock().unwrap() = "file:///tmp/video.mp4".to_string();
let out = std::env::temp_dir().join("oak-otio-out.otio");
let out_str = out.to_string_lossy().into_owned();
let _ = std::fs::remove_file(&out);
let mut task =
unsafe { oaktask_create_project_save_otio(fake_handle(), common::cstr_of(&out_str)) };
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert!(out.exists(), "OTIO file was not written");
// Failure paths: null project or null filename.
assert!(unsafe {
oaktask_create_project_save_otio(CHandle::null(), common::cstr_of("/tmp/x.otio"))
}
.ctx
.is_null());
assert!(
unsafe { oaktask_create_project_save_otio(fake_handle(), std::ptr::null()) }
.ctx
.is_null()
);
free(&mut task);
let _ = std::fs::remove_file(&out);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// Given a footage/sequence pair, `create_precache` runs to completion and
/// succeeds.
#[test]
fn precache_runs_and_succeeds() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
FOOTAGE_FILENAME.lock().unwrap().clear();
let mut task = unsafe { oaktask_create_precache(fake_handle(), 0, fake_handle()) };
assert!(!task.ctx.is_null());
let needed = unsafe { oaktask_task_title(task, std::ptr::null_mut(), 0) };
assert!(needed > 0);
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1);
// Failure path: null footage/sequence.
assert!(
unsafe { oaktask_create_precache(CHandle::null(), 0, fake_handle()) }
.ctx
.is_null()
);
assert!(
unsafe { oaktask_create_precache(fake_handle(), 0, CHandle::null()) }
.ctx
.is_null()
);
free(&mut task);
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// create_export runs the full render+encoder path against the stubs and
/// succeeds; the encoder-failure path fails cleanly.
#[test]
fn export_runs_with_stubbed_encoder() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
*PROJECT_FILENAME.lock().unwrap() = "/tmp/oak-export-test.mp4".to_string();
NODE_LABEL.lock().unwrap().clear();
let mut params = oaktask::bridge::codec::OakCodecEncodingParams {
filename: [0; 1024],
format: 0,
video_enabled: 1,
video_codec: 0,
video_width: 1920,
video_height: 1080,
video_time_base_num: 1,
video_time_base_den: 25,
video_pixel_format: 0,
video_interlacing: 0,
video_pixel_aspect_num: 1,
video_pixel_aspect_den: 1,
video_bit_rate: 0,
video_min_bit_rate: 0,
video_max_bit_rate: 0,
video_buffer_size: 0,
video_threads: 0,
video_pix_fmt: [0; 64],
video_is_image_sequence: 0,
video_scaling_method: 0,
audio_enabled: 0,
audio_codec: 0,
audio_sample_rate: 0,
audio_channel_layout: 0,
audio_sample_format: 0,
audio_bit_rate: 0,
subtitles_enabled: 0,
subtitles_codec: 0,
subtitles_are_sidecar: 0,
subtitles_sidecar_format: 0,
color_transform_output: [0; 256],
export_length_num: 0,
export_length_den: 0,
has_custom_range: 0,
custom_range_in_num: 0,
custom_range_in_den: 0,
custom_range_out_num: 0,
custom_range_out_den: 0,
};
let filename = std::env::temp_dir()
.join("oak-export-test.mp4")
.to_string_lossy()
.into_owned();
write_cstr(&filename, params.filename.as_mut_ptr(), 1024);
ENCODER_INIT_NULL.store(0, Ordering::SeqCst);
ENCODER_OPEN_RESULT.store(0, Ordering::SeqCst);
ENCODER_WRITE_VIDEO_RESULT.store(0, Ordering::SeqCst);
SEQUENCE_LENGTH_NUM.store(25, Ordering::SeqCst);
SEQUENCE_LENGTH_DEN.store(1, Ordering::SeqCst);
VIDEO_TIME_BASE_NUM.store(1, Ordering::SeqCst);
VIDEO_TIME_BASE_DEN.store(1, Ordering::SeqCst); // 1s frames -> 25 frames
let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), &params) };
assert!(!task.ctx.is_null());
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 1);
assert_eq!(unsafe { oaktask_task_is_finished(task) }, 1);
free(&mut task);
// Failure path: encoder init fails.
ENCODER_INIT_NULL.store(1, Ordering::SeqCst);
let mut task = unsafe { oaktask_create_export(fake_handle(), fake_handle(), &params) };
assert_eq!(unsafe { oaktask_task_start_sync(task) }, 0);
let needed = unsafe { oaktask_task_error(task, std::ptr::null_mut(), 0) };
assert!(needed > 0);
let mut buf = vec![0i8; needed as usize];
unsafe { oaktask_task_error(task, buf.as_mut_ptr(), needed) };
assert!(cstr_read(&buf).contains("Failed to create encoder"));
free(&mut task);
// Failure path: null viewer / null params.
assert!(
unsafe { oaktask_create_export(CHandle::null(), fake_handle(), &params) }
.ctx
.is_null()
);
assert!(
unsafe { oaktask_create_export(fake_handle(), fake_handle(), std::ptr::null()) }
.ctx
.is_null()
);
reset_stubs();
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// The manager is needed for nothing in these factories; a fresh
/// init/shutdown cycle leaves the alive count at zero.
#[test]
fn manager_cycle_leaves_no_leaks() {
let _guard = MANAGER_LOCK.lock().unwrap();
reset_stubs();
unsafe { oaktask_manager_shutdown() };
assert_eq!(unsafe { oaktask_manager_init() }, 0);
unsafe { oaktask_manager_shutdown() };
assert_eq!(unsafe { oaktask_debug_alive_count() }, 0);
}
/// cstr_read helper: read a NUL-terminated c_char buffer.
fn cstr_read(buf: &[i8]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
String::from_utf8_lossy(bytes).into_owned()
}