fix(oakengine): facade bugs found by integration tests
undo: - NULL/empty label no longer crosses to oakundo as a dangling 0x1 pointer (push, group_begin/end) — fixed SIGSEGV - group_abort now undoes each executed child in reverse order task: - create_project_import addrefs the borrowed project handle instead of freeing it under the async task — fixed UAF/SIGSEGV timeline: - toggle_enabled/delete_clips guard NULL+0 slices — fixed SIGABRT - BlockSplitCommand halves placed correctly (oaktimeline undosplit) - PreservingLinks / ripple remove / ripple delete-gaps commands self-prepare on first redo — fixes silent no-op split/ripple - trim_clips_to targets the block containing the point, not the track - delete_empty_tracks applies the live track removal - ripple facades no longer free borrowed track handles still referenced by commands — fixed UAF node: - project_add_node releases the factory handle — fixes per-call leak - inputs_from(recursive=0) matches direct feeders (BFS off-by-one) - group passthrough id/resolve treat two-stage string length as success - node_connect(_command) reject duplicate connects with E_STATE - folder_add_child enforces one-folder-per-node - value_split_to_tracks splits vector/color per component - set_context_position/expanded establish the first entry - node_get_flags on an empty box returns 0, not u64::MAX - footage_borrow addrefs its wrapper — fixes double-free render: - renderer_create rejects invalid pixel formats (real range check) - render_frame forwards renderer width/height to the ticket tests: repro #[ignore]s removed, bug-behavior assertions corrected, it_undo global-stack tests serialized with a shared lock
This commit is contained in:
@@ -911,6 +911,22 @@ pub unsafe extern "C" fn oakengine_folder_add_child(
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let c = unbox(child)?;
|
||||
// Mirror the module's live one-folder-per-node check (its UNDOABLE
|
||||
// FolderAddChild command creator skips it): a node already in
|
||||
// another folder is rejected with the module STATE error.
|
||||
let parent = n::oaknode_folder_parent_of(c);
|
||||
if !parent.ctx.is_null() {
|
||||
let already_here =
|
||||
n::oaknode_node_identity(parent) != 0
|
||||
&& n::oaknode_node_identity(parent) == n::oaknode_node_identity(f);
|
||||
// The borrowed parent handle's shell is released here.
|
||||
if let Some(release) = parent.release {
|
||||
unsafe { release(parent.ctx) };
|
||||
}
|
||||
if !already_here {
|
||||
return Err(Error::Module(oaknode::error::OAKNODE_E_STATE));
|
||||
}
|
||||
}
|
||||
let cmd = n::oaknode_command_create_folder_add_child(f, c);
|
||||
if cmd.ctx.is_null() {
|
||||
return Err(Error::Failed("folder add child command failed".into()));
|
||||
@@ -1340,12 +1356,16 @@ pub unsafe extern "C" fn oakengine_node_category_at(
|
||||
/// `oakengine_node_get_flags`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_node_get_flags(self_: *const OakEngineNode) -> u64 {
|
||||
// Stub: the oaknode module has no per-node flags export.
|
||||
// Stub: the oaknode module has no per-node flags export. NULL and
|
||||
// empty (null-ctx) handle boxes both report 0 — the `guard_i64`
|
||||
// error sentinel would otherwise surface as u64::MAX to C callers.
|
||||
crate::handle::guard_i64(|| unsafe {
|
||||
if self_.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
let _ = unbox(self_)?;
|
||||
if (*self_).handle.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
Ok(0)
|
||||
}) as u64
|
||||
}
|
||||
@@ -2188,6 +2208,12 @@ pub unsafe extern "C" fn oakengine_project_add_node(
|
||||
continue;
|
||||
}
|
||||
if is_node_type(other, &type_id_str) {
|
||||
// The AddNode command MOVED the node into the project graph,
|
||||
// so the factory's owned handle is now just a stale view:
|
||||
// release it (the node itself stays graph-owned) so the
|
||||
// debug alive counter returns to baseline.
|
||||
let mut owned = node;
|
||||
n::oaknode_node_free(&mut owned);
|
||||
return Ok(box_handle::<OakEngineNode>(other));
|
||||
}
|
||||
}
|
||||
@@ -2262,6 +2288,20 @@ pub unsafe extern "C" fn oakengine_node_connect(
|
||||
}
|
||||
let out_h = unbox(output_node)?;
|
||||
let in_h = unbox(input_node)?;
|
||||
// Mirror the module's live connect rejection: an already-connected
|
||||
// input is a STATE error. The UNDOABLE creator validates existence
|
||||
// and connectability but not "already connected" (its redo swallows
|
||||
// the state error), so the facade pre-checks like the live variant.
|
||||
let mut connected: c_int = 0;
|
||||
Error::from_module(n::oaknode_node_input_is_connected(
|
||||
in_h,
|
||||
input_id,
|
||||
&mut connected,
|
||||
))?;
|
||||
if connected != 0 {
|
||||
set_node_error("input is already connected");
|
||||
return Err(Error::Module(oaknode::error::OAKNODE_E_STATE));
|
||||
}
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
let rc = n::oaknode_node_connect_undoable(out_h, in_h, input_id, &mut cmd);
|
||||
if rc != 0 {
|
||||
@@ -2325,6 +2365,16 @@ pub unsafe extern "C" fn oakengine_node_connect_command(
|
||||
let out_h = unbox(output_node)?;
|
||||
let in_h = unbox(input_node)?;
|
||||
let _ = element;
|
||||
// Same duplicate-connect rejection as `oakengine_node_connect`.
|
||||
let mut connected: c_int = 0;
|
||||
Error::from_module(n::oaknode_node_input_is_connected(
|
||||
in_h,
|
||||
input_id,
|
||||
&mut connected,
|
||||
))?;
|
||||
if connected != 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
let rc = n::oaknode_node_connect_undoable(out_h, in_h, input_id, &mut cmd);
|
||||
if rc != 0 {
|
||||
@@ -3588,6 +3638,16 @@ pub unsafe extern "C" fn oakengine_node_set_context_position(
|
||||
}
|
||||
let ch = unbox(context)?;
|
||||
let nh = unbox(node)?;
|
||||
// The module's undoable setter requires a pre-existing
|
||||
// context_positions entry (else NOT_FOUND); create the first entry
|
||||
// with the live setter so positions can be ESTABLISHED through the
|
||||
// facade (bug fix: they were previously impossible to create).
|
||||
let mut x0: f64 = 0.0;
|
||||
let mut y0: f64 = 0.0;
|
||||
let mut e0: c_int = 0;
|
||||
if n::oaknode_node_get_context_position(nh, ch, &mut x0, &mut y0, &mut e0) != 0 {
|
||||
Error::from_module(n::oaknode_node_set_context_position(nh, ch, 0.0, 0.0, 0))?;
|
||||
}
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
let rc = n::oaknode_node_set_context_position_undoable(nh, ch, x, y, 0, &mut cmd);
|
||||
if rc != 0 {
|
||||
@@ -3642,7 +3702,9 @@ pub unsafe extern "C" fn oakengine_node_set_context_expanded(
|
||||
let mut was: c_int = 0;
|
||||
let rc = n::oaknode_node_get_context_position(nh, ch, &mut x, &mut y, &mut was);
|
||||
if rc != 0 {
|
||||
return Err(Error::Module(rc));
|
||||
// No entry yet: establish one (the module's undoable setter
|
||||
// below demands a pre-existing context_positions entry).
|
||||
Error::from_module(n::oaknode_node_set_context_position(nh, ch, 0.0, 0.0, 0))?;
|
||||
}
|
||||
let mut cmd: CHandle = CHandle::null();
|
||||
let rc = n::oaknode_node_set_context_position_undoable(
|
||||
@@ -3730,7 +3792,9 @@ pub unsafe extern "C" fn oakengine_node_group_get_inner(
|
||||
out_input.len() as c_int,
|
||||
&mut out_element,
|
||||
);
|
||||
if rc != 0 || out_node.is_null() {
|
||||
// The module getter returns the copied string length (>= 0) on
|
||||
// success; only negative codes are failures.
|
||||
if rc < 0 || out_node.is_null() {
|
||||
return Ok(0);
|
||||
}
|
||||
// One level only: if the resolved node is the same, nothing moved.
|
||||
@@ -3882,7 +3946,9 @@ pub unsafe extern "C" fn oakengine_group_get_id_of_passthrough(
|
||||
out_input.len() as c_int,
|
||||
&mut out_element,
|
||||
);
|
||||
if rc != 0 || out_node.is_null() {
|
||||
// The module getter returns the copied string length (>= 0) on
|
||||
// success; only negative codes are failures.
|
||||
if rc < 0 || out_node.is_null() {
|
||||
continue;
|
||||
}
|
||||
if n::oaknode_node_identity(out_node) == n::oaknode_node_identity(ih)
|
||||
@@ -3936,9 +4002,14 @@ pub unsafe extern "C" fn oakengine_group_get_passthrough_from_id(
|
||||
out_input_size,
|
||||
out_element,
|
||||
);
|
||||
if rc != 0 || node.is_null() {
|
||||
// The module getter returns the copied string length (>= 0) on
|
||||
// success; only negative codes are failures.
|
||||
if rc < 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
if node.is_null() {
|
||||
continue;
|
||||
}
|
||||
if !out_node.is_null() {
|
||||
*out_node = box_handle::<OakEngineNode>(node);
|
||||
}
|
||||
@@ -4016,7 +4087,9 @@ pub unsafe extern "C" fn oakengine_group_resolve_input(
|
||||
out_input_size,
|
||||
out_element,
|
||||
);
|
||||
if rc != 0 {
|
||||
// The module getter returns the copied string length (>= 0) on
|
||||
// success; only negative codes are failures.
|
||||
if rc < 0 {
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
if !out_node.is_null() {
|
||||
@@ -5589,7 +5662,9 @@ pub unsafe extern "C" fn oakengine_node_value_split_to_tracks(
|
||||
t.den = n.den;
|
||||
}
|
||||
value_type::COLOR | value_type::VEC2 | value_type::VEC3 | value_type::VEC4 => {
|
||||
t.f = n.f;
|
||||
// Per-component split: track `i` carries component `i`
|
||||
// (combine_tracks reassembles from each track's f[0]).
|
||||
t.f = [n.f[i], 0.0, 0.0, 0.0];
|
||||
}
|
||||
value_type::FLOAT | value_type::BEZIER => {
|
||||
t.f[0] = n.f[0];
|
||||
@@ -6013,7 +6088,10 @@ pub unsafe extern "C" fn oakengine_node_inputs_from(
|
||||
let sh = unbox(self_)?;
|
||||
let oh = unbox(other)?;
|
||||
// BFS over the module's output connections starting at `other`
|
||||
// (inputs_from: is `other` reachable feeding into `self`?).
|
||||
// (inputs_from: is `other` reachable feeding into `self`?). Every
|
||||
// discovered neighbor is checked against the target, so a DIRECT
|
||||
// feeder is found while expanding the depth-0 frontier; recursion
|
||||
// merely widens the search beyond it.
|
||||
let target = n::oaknode_node_identity(sh);
|
||||
let mut frontier = vec![oh];
|
||||
let mut visited: Vec<usize> = Vec::new();
|
||||
@@ -6038,6 +6116,12 @@ pub unsafe extern "C" fn oakengine_node_inputs_from(
|
||||
if n::oaknode_node_output_connection_node_at(cur, i, &mut out) == 0
|
||||
&& !out.is_null()
|
||||
{
|
||||
// Direct feeders are identified at discovery, before
|
||||
// the depth counter advances (recursive == 0 must
|
||||
// still inspect `other`'s own outputs).
|
||||
if n::oaknode_node_identity(out) == target {
|
||||
return Ok(1);
|
||||
}
|
||||
next.push(out);
|
||||
}
|
||||
}
|
||||
@@ -6731,11 +6815,16 @@ pub unsafe extern "C" fn oakengine_footage_borrow(
|
||||
if node.is_null() {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let h = unbox(node)?;
|
||||
let mut h = unbox(node)?;
|
||||
if !is_node_type(h, TYPE_ID_FOOTAGE) {
|
||||
set_footage_error("node is not a footage node");
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
// The borrow takes its OWN reference (addref) so freeing both the
|
||||
// borrow and the source node shell later is double-free-safe.
|
||||
if let Some(addref) = h.addref {
|
||||
unsafe { addref(h.ctx) };
|
||||
}
|
||||
Ok(box_handle::<OakEngineFootage>(h))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -193,12 +193,13 @@ pub unsafe extern "C" fn oakengine_renderer_create(
|
||||
if seq.is_null() || width <= 0 || height <= 0 || frame_rate_num <= 0 || frame_rate_den <= 0 {
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
// Validate the pixel format against the oakcommon format enum.
|
||||
if crate::bridge::common::oakcommon_videoparams_get_format_name(
|
||||
pixel_format,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
) < 0
|
||||
// Validate the pixel format against the oakcore enum. The
|
||||
// oakcommon format_name lookup succeeds for ANY code (unknowns
|
||||
// format as "Unknown (0x…)"), so only the real formats (U8..F32)
|
||||
// are accepted; Invalid (-1), the Count sentinel (5) and garbage
|
||||
// codes are rejected.
|
||||
if pixel_format < oakcore_rs::PixelFormat::U8 as c_int
|
||||
|| pixel_format > oakcore_rs::PixelFormat::F32 as c_int
|
||||
{
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
@@ -275,8 +276,8 @@ pub unsafe extern "C" fn oakengine_renderer_render_frame(
|
||||
time_den: i64::from(b.frame_rate_num),
|
||||
color_manager: CHandle::null(),
|
||||
mode: b.mode,
|
||||
force_width: 0,
|
||||
force_height: 0,
|
||||
force_width: b.width,
|
||||
force_height: b.height,
|
||||
force_matrix: [0.0; 16],
|
||||
has_force_matrix: 0,
|
||||
force_format: -1,
|
||||
|
||||
@@ -82,6 +82,11 @@ struct TaskMeta {
|
||||
/// The project a save task writes (addref'd at creation, released at
|
||||
/// free) — the module has no save-project getter.
|
||||
save_project: Option<CHandle>,
|
||||
/// The project an import task borrows (addref'd at creation, released
|
||||
/// at free): the module's import task stores its project handle WITHOUT
|
||||
/// addref, so this facade-side ref keeps the shared box alive while the
|
||||
/// task runs (see `oakengine_task_create_project_import`).
|
||||
import_project: Option<CHandle>,
|
||||
/// The encoding-params box an export task owns, dropped at free
|
||||
/// (mirrors the C++ `FacadeExportTask` destructor; stored as `usize` so
|
||||
/// the map stays `Send`).
|
||||
@@ -97,6 +102,7 @@ impl TaskMeta {
|
||||
started: false,
|
||||
cancelled: false,
|
||||
save_project: None,
|
||||
import_project: None,
|
||||
export_params: None,
|
||||
export_color_manager: None,
|
||||
}
|
||||
@@ -141,13 +147,16 @@ fn meta_set_cancelled(key: usize) {
|
||||
}
|
||||
|
||||
/// Release every facade-side sidecar of a task (called by
|
||||
/// [`oakengine_task_free`]): the addref'd save project, the owned
|
||||
/// [`oakengine_task_free`]): the addref'd save/import projects, the owned
|
||||
/// encoding-params box and the derived color manager of an export task.
|
||||
fn drop_task_meta(key: usize) {
|
||||
if let Some(meta) = meta_lock().remove(&key) {
|
||||
if let Some(mut project) = meta.save_project {
|
||||
unsafe { n::oaknode_project_free(&mut project) };
|
||||
}
|
||||
if let Some(mut project) = meta.import_project {
|
||||
unsafe { n::oaknode_project_free(&mut project) };
|
||||
}
|
||||
if let Some(ptr) = meta.export_params {
|
||||
unsafe {
|
||||
crate::codec::oakengine_encoding_params_destroy(ptr as *mut OakEngineEncodingParams)
|
||||
@@ -534,6 +543,13 @@ fn project_filename_of(project: CHandle) -> Result<String> {
|
||||
/// `oakengine_task_import_file_count` documents 0 as "nothing to import,
|
||||
/// free instead of run". `url_count < 0`, a NULL URL inside the array, or a
|
||||
/// folder with no project yield NULL.
|
||||
///
|
||||
/// The module's import task stores the project handle WITHOUT addref, so
|
||||
/// the facade keeps an addref'd copy in [`TaskMeta::import_project`]
|
||||
/// (released at free) — without it, releasing the transient borrowed
|
||||
/// handle here would drop the shared box while the task still references
|
||||
/// it, and the run would read freed memory (the former SIGSEGV reproduced
|
||||
/// by `it_task::import_run_single_file`).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakengine_task_create_project_import(
|
||||
folder: *mut OakEngineNode,
|
||||
@@ -551,10 +567,22 @@ pub unsafe extern "C" fn oakengine_task_create_project_import(
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
let h = t::oaktask_create_project_import(fh, project, urls, url_count);
|
||||
// Release the transient borrowed project handle (the import task
|
||||
// keeps its own copy).
|
||||
if h.is_null() {
|
||||
// Creation failed: release the transient borrowed handle.
|
||||
n::oaknode_project_free(&mut project);
|
||||
return Ok(std::ptr::null_mut());
|
||||
}
|
||||
// Keep the project borrowed for the task's lifetime (the module's
|
||||
// import task stores the handle without addref): addref before the
|
||||
// transient handle below is released, so the shared box stays alive
|
||||
// until `oakengine_task_free` drops the meta-side copy.
|
||||
let mut meta = TaskMeta::new();
|
||||
meta.import_project = Some(project.addref());
|
||||
// Release the transient borrowed project handle (the task's copy and
|
||||
// the facade-side addref above keep the box alive).
|
||||
n::oaknode_project_free(&mut project);
|
||||
Ok(box_task(h))
|
||||
meta_insert(h.ctx as usize, meta);
|
||||
Ok(box_handle::<OakEngineTask>(h))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2158,7 +2158,11 @@ pub unsafe extern "C" fn oakengine_sequence_ripple_delete_clip(
|
||||
out_num as i64,
|
||||
out_den as i64,
|
||||
);
|
||||
release_handle(track);
|
||||
// NOTE: `track` is intentionally NOT released — the module command
|
||||
// stores the borrowed handle for its whole lifetime (its `redo`/
|
||||
// `undo` re-resolve the track), and the module model keeps such
|
||||
// handles alive for the command's lifetime (same as
|
||||
// `oakengine_sequence_delete_clips`).
|
||||
if cmd.is_null() {
|
||||
set_seq_error("ripple delete command failed");
|
||||
return Err(Error::Failed("ripple delete command failed".into()));
|
||||
@@ -2450,7 +2454,14 @@ pub unsafe extern "C" fn oakengine_sequence_delete_clips(
|
||||
// (track, in, out) rationals of the deleted clips, for the default
|
||||
// ripple regions.
|
||||
let mut clip_ranges: Vec<(CHandle, i64, i64, i64, i64)> = Vec::new();
|
||||
let slice = std::slice::from_raw_parts(clips, clip_count.max(0) as usize);
|
||||
// NULL with a zero count is a legal empty set; the slice must not be
|
||||
// constructed from the NULL pointer (`slice::from_raw_parts(NULL, 0)`
|
||||
// is UB), so it is only built for a positive count.
|
||||
let slice: &[*mut OakEngineClip] = if clip_count > 0 {
|
||||
std::slice::from_raw_parts(clips, clip_count as usize)
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
for (i, clip) in slice.iter().enumerate() {
|
||||
let c = match unbox(*clip) {
|
||||
Ok(h) => h,
|
||||
@@ -2610,7 +2621,9 @@ pub unsafe extern "C" fn oakengine_sequence_ripple_delete_range(
|
||||
out_num,
|
||||
out_den,
|
||||
);
|
||||
release_handle(track);
|
||||
// NOTE: `track` is intentionally NOT released — the module
|
||||
// command stores the borrowed handle for its whole lifetime (see
|
||||
// `oakengine_sequence_ripple_delete_clip`).
|
||||
if cmd.is_null() {
|
||||
return Err(Error::Failed("ripple delete command failed".into()));
|
||||
}
|
||||
@@ -2634,7 +2647,14 @@ pub unsafe extern "C" fn oakengine_clip_toggle_enabled(
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let mut children: Vec<CHandle> = Vec::new();
|
||||
let slice = std::slice::from_raw_parts(clips, count as usize);
|
||||
// NULL with a zero count is a legal empty set; the slice must not be
|
||||
// constructed from the NULL pointer (`slice::from_raw_parts(NULL, 0)`
|
||||
// is UB), so it is only built for a positive count.
|
||||
let slice: &[*mut OakEngineClip] = if count > 0 {
|
||||
std::slice::from_raw_parts(clips, count as usize)
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
for (i, clip) in slice.iter().enumerate() {
|
||||
let c = match unbox(*clip) {
|
||||
Ok(h) => h,
|
||||
@@ -2843,7 +2863,9 @@ pub unsafe extern "C" fn oakengine_sequence_ripple_delete_in_to_out(
|
||||
out_num,
|
||||
out_den,
|
||||
);
|
||||
release_handle(track);
|
||||
// NOTE: `track` is intentionally NOT released — the module
|
||||
// command stores the borrowed handle for its whole lifetime
|
||||
// (see `oakengine_sequence_ripple_delete_clip`).
|
||||
if cmd.is_null() {
|
||||
release_handle(wa);
|
||||
return Err(Error::Failed("ripple remove command failed".into()));
|
||||
@@ -2977,19 +2999,18 @@ pub unsafe extern "C" fn oakengine_sequence_trim_clips_to(
|
||||
release_handle(track);
|
||||
continue;
|
||||
}
|
||||
// A trim (in or out) is only meaningful for the block that
|
||||
// CONTAINS the point (in < point < out); the nearest-before
|
||||
// queries can pick an insertion-order neighbor that ends before
|
||||
// the point (which would trim to a negative length), so the
|
||||
// strictly-containing lookup is used for both modes.
|
||||
let mut block = CHandle::null();
|
||||
let rc = if mode == MOVEMENT_MODE_TRIM_IN {
|
||||
n::oaknode_track_get_nearest_block_before_or_at(
|
||||
track,
|
||||
point_num as c_int,
|
||||
point_den as c_int,
|
||||
&mut block,
|
||||
)
|
||||
} else {
|
||||
// The module exports no plain before query; iterate.
|
||||
block = nearest_block_before(track, point_num, point_den);
|
||||
if block.is_null() { -1 } else { 0 }
|
||||
};
|
||||
let rc = n::oaknode_track_get_block_containing_time(
|
||||
track,
|
||||
point_num as c_int,
|
||||
point_den as c_int,
|
||||
&mut block,
|
||||
);
|
||||
if rc != 0 || block.is_null() {
|
||||
release_handle(track);
|
||||
continue;
|
||||
@@ -3007,18 +3028,6 @@ pub unsafe extern "C" fn oakengine_sequence_trim_clips_to(
|
||||
let mut out_den: c_int = 0;
|
||||
n::oaknode_block_get_in(block, &mut in_num, &mut in_den);
|
||||
n::oaknode_block_get_out(block, &mut out_num, &mut out_den);
|
||||
let nearest_time = if mode == MOVEMENT_MODE_TRIM_IN {
|
||||
(in_num as i64, in_den as i64)
|
||||
} else {
|
||||
(out_num as i64, out_den as i64)
|
||||
};
|
||||
if rat_cmp(nearest_time.0, nearest_time.1, point_num, point_den)
|
||||
== std::cmp::Ordering::Equal
|
||||
{
|
||||
release_handle(block);
|
||||
release_handle(track);
|
||||
continue;
|
||||
}
|
||||
// new_length = length - |nearest_time - point|; the in-trim
|
||||
// anchors the out, the out-trim anchors the in (see
|
||||
// `oakengine_clip_trim`).
|
||||
@@ -3030,8 +3039,11 @@ pub unsafe extern "C" fn oakengine_sequence_trim_clips_to(
|
||||
let mut old_len_num: c_int = 0;
|
||||
let mut old_len_den: c_int = 0;
|
||||
Error::from_module(n::oaknode_block_get_length(block, &mut old_len_num, &mut old_len_den))?;
|
||||
// Trim the addressed block itself (`trim_cmd` anchors on the
|
||||
// block handle; passing the track used to silently reject the
|
||||
// trim in the module).
|
||||
let cmd = trim_cmd(
|
||||
track,
|
||||
block,
|
||||
mode,
|
||||
old_len_num,
|
||||
old_len_den,
|
||||
@@ -3071,6 +3083,10 @@ pub unsafe extern "C" fn oakengine_sequence_delete_empty_tracks(
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let mut children: Vec<CHandle> = Vec::new();
|
||||
// (track, owning list) pairs for the live removal compensation
|
||||
// (the module's `TimelineRemoveTrackCommand` redo is a no-op for the
|
||||
// list structure; see below).
|
||||
let mut to_remove: Vec<(CHandle, CHandle)> = Vec::new();
|
||||
let mut removed: c_int = 0;
|
||||
let mut all_count: c_int = 0;
|
||||
Error::from_module(n::oaknode_sequence_get_all_track_count(sequence, &mut all_count))?;
|
||||
@@ -3095,6 +3111,17 @@ pub unsafe extern "C" fn oakengine_sequence_delete_empty_tracks(
|
||||
continue;
|
||||
}
|
||||
let cmd = tl::oaktimeline_remove_track_command(track);
|
||||
// Locate the owning list for the live removal (addref the track
|
||||
// first so it survives the release below).
|
||||
let mut ttype: c_int = 0;
|
||||
if n::oaknode_track_get_type(track, &mut ttype) == 0 {
|
||||
let mut list = CHandle::null();
|
||||
if n::oaknode_sequence_get_track_list(sequence, ttype, &mut list) == 0
|
||||
&& !list.is_null()
|
||||
{
|
||||
to_remove.push((track.addref(), list));
|
||||
}
|
||||
}
|
||||
release_handle(track);
|
||||
if cmd.is_null() {
|
||||
return Err(Error::Failed("remove track command failed".into()));
|
||||
@@ -3106,6 +3133,14 @@ pub unsafe extern "C" fn oakengine_sequence_delete_empty_tracks(
|
||||
return Ok(0);
|
||||
}
|
||||
push_multi_commands(&children, "Delete Empty Tracks")?;
|
||||
// The module's TimelineRemoveTrackCommand redo is a no-op for the
|
||||
// list structure (undogeneral.rs NOTE), so the removal is applied
|
||||
// live as compensation (the same documented deviation as
|
||||
// `oakengine_sequence_remove_track`).
|
||||
for (track, list) in &to_remove {
|
||||
n::oaknode_tracklist_remove_track(*list, *track);
|
||||
release_handle(*list);
|
||||
}
|
||||
Ok(removed)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,7 +93,15 @@ pub(crate) unsafe fn push_or_run(command_box: *mut OakEngineClipboard, name: *co
|
||||
return if rc == 0 { Ok(()) } else { Err(Error::Module(rc)) };
|
||||
}
|
||||
let stack = *global_stack();
|
||||
let rc = unsafe { u::oakundo_undostack_push(stack, cmd, label.as_ptr() as *const c_char) };
|
||||
// The module treats a NULL name like an empty label, but an empty Rust
|
||||
// String's `as_ptr()` is a DANGLING non-NULL pointer (0x1): the module's
|
||||
// `read_name` would strlen it and SIGSEGV. Pass a real NULL instead.
|
||||
let label_ptr = if label.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
label.as_ptr() as *const c_char
|
||||
};
|
||||
let rc = unsafe { u::oakundo_undostack_push(stack, cmd, label_ptr) };
|
||||
if rc == 0 {
|
||||
// Stack took a reference; release ours by freeing the box.
|
||||
unsafe { free_box(command_box) };
|
||||
@@ -156,13 +164,19 @@ pub extern "C" fn oakengine_undo_group_end() -> c_int {
|
||||
let multi = open.multi;
|
||||
let name = open.name;
|
||||
drop(g);
|
||||
// Same NULL-for-empty convention as `push_or_run`: the module's
|
||||
// `read_name` treats NULL like an empty label, while an empty String's
|
||||
// dangling `as_ptr()` (0x1) would be strlen'd -> SIGSEGV.
|
||||
let name_ptr = if name.is_empty() {
|
||||
std::ptr::null()
|
||||
} else {
|
||||
name.as_ptr() as *const c_char
|
||||
};
|
||||
// push_pre_executed discards an empty multi command. Either way
|
||||
// the stack took (or destroyed) the command; release our own
|
||||
// reference to the multi handle.
|
||||
let stack = *global_stack();
|
||||
let rc = unsafe {
|
||||
u::oakundo_undostack_push_pre_executed(stack, multi, name.as_ptr() as *const c_char)
|
||||
};
|
||||
let rc = unsafe { u::oakundo_undostack_push_pre_executed(stack, multi, name_ptr) };
|
||||
let mut multi_handle = multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi_handle) };
|
||||
if rc == 0 {
|
||||
@@ -181,10 +195,37 @@ pub extern "C" fn oakengine_undo_group_abort() -> c_int {
|
||||
let mut g = group_lock();
|
||||
let open = g.take().ok_or(Error::State)?;
|
||||
drop(g);
|
||||
let rc = unsafe { u::oakundo_command_undo_now(open.multi) };
|
||||
// The multi command itself is never marked done (each child was
|
||||
// redo'd eagerly at push time), so `undo_now` on it is a no-op.
|
||||
// Undo the executed children individually instead, in reverse
|
||||
// insertion order (mirroring the multi's reverse-order undo), each
|
||||
// through its own borrowed handle.
|
||||
let mut count: c_int = 0;
|
||||
let rc = unsafe { u::oakundo_command_multi_child_count(open.multi, &mut count) };
|
||||
if rc != 0 {
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
for i in (0..count).rev() {
|
||||
let mut child = CHandle::null();
|
||||
let rc = unsafe { u::oakundo_command_multi_child(open.multi, i, &mut child) };
|
||||
if rc != 0 {
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
let rc = unsafe { u::oakundo_command_undo_now(child) };
|
||||
// The child handle is borrowed (owns:false): release only its
|
||||
// shell — the child value lives on in the multi until the multi
|
||||
// itself is freed below.
|
||||
unsafe { u::oakundo_command_free(&mut child) };
|
||||
if rc != 0 {
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
return Err(Error::Module(rc));
|
||||
}
|
||||
}
|
||||
let mut multi = open.multi;
|
||||
unsafe { u::oakundo_command_free(&mut multi) };
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user