ztmux 0.1.0

A Rust port of tmux — the full terminal multiplexer, server and client
Documentation
// Copyright (c) 2008, 2017 Otto Moerbeek <otto@drijf.net>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
use core::ffi::c_void;
use core::ptr::null_mut;

/// C `vendor/tmux/compat/recallocarray.c:33`: `void *recallocarray(void *ptr, size_t oldnmemb, size_t newnmemb, size_t size)`
pub unsafe fn recallocarray(
    ptr: *mut c_void,
    oldnmemb: usize,
    newnmemb: usize,
    size: usize,
) -> *mut c_void {
    const MUL_NO_OVERFLOW: usize = 1usize << (size_of::<usize>() * 4);

    unsafe extern "C" {
        fn getpagesize() -> i32;
    }

    unsafe {
        if ptr.is_null() {
            return libc::calloc(newnmemb, size);
        }

        if (newnmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW)
            && newnmemb > 0
            && usize::MAX / newnmemb < size
        {
            crate::errno!() = libc::ENOMEM;
            return null_mut();
        }
        let newsize = newnmemb * size;

        if (oldnmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW)
            && oldnmemb > 0
            && usize::MAX / oldnmemb < size
        {
            crate::errno!() = libc::EINVAL;
            return null_mut();
        }
        let oldsize = oldnmemb * size;

        // Don't bother too much if we're shrinking just a bit,
        // we do not shrink for series of small steps, oh well.
        if newsize <= oldsize {
            let d = oldsize - newsize;

            if d < oldsize / 2 && d < getpagesize() as usize {
                libc::memset((ptr as *mut u8).add(newsize).cast(), 0, d);
                return ptr;
            }
        }

        let newptr = libc::malloc(newsize);
        if newptr.is_null() {
            return null_mut();
        }

        if newsize > oldsize {
            libc::memcpy(newptr, ptr, oldsize);
            libc::memset(
                (newptr as *mut u8).add(oldsize).cast(),
                0,
                newsize - oldsize,
            );
        } else {
            libc::memcpy(newptr, ptr, newsize);
        }

        libc::memset(ptr, 0, oldsize);
        libc::free(ptr);

        newptr
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Expected behavior derived from vendor/tmux/compat/recallocarray.c.

    #[test]
    fn test_recallocarray_null_ptr_is_calloc() {
        // C: `if (ptr == NULL) return calloc(newnmemb, size);`
        // Result is newnmemb*size bytes, all zeroed.
        unsafe {
            let p = recallocarray(null_mut(), 0, 5, 4) as *mut u8;
            assert!(!p.is_null());
            for i in 0..20 {
                assert_eq!(*p.add(i), 0, "calloc'd byte {i} not zero");
            }
            libc::free(p.cast());
        }
    }

    #[test]
    fn test_recallocarray_grow_zeroes_new_and_preserves_old() {
        // Growing goes through the malloc/memcpy/memset path:
        // new bytes [oldsize..newsize) must be zeroed, old bytes copied.
        unsafe {
            let p = libc::malloc(4) as *mut u8;
            assert!(!p.is_null());
            for i in 0..4 {
                *p.add(i) = 0xAA;
            }

            let q = recallocarray(p.cast(), 4, 8, 1) as *mut u8;
            assert!(!q.is_null());
            // Original bytes preserved.
            for i in 0..4 {
                assert_eq!(*q.add(i), 0xAA, "old byte {i} not preserved");
            }
            // Newly grown bytes zeroed.
            for i in 4..8 {
                assert_eq!(*q.add(i), 0, "grown byte {i} not zeroed");
            }
            libc::free(q.cast());
        }
    }

    #[test]
    fn test_recallocarray_small_shrink_zeroes_tail_in_place() {
        // C: when newsize <= oldsize and the delta is small (d < oldsize/2 and
        // d < getpagesize()), the same ptr is returned with the freed tail
        // [newsize..oldsize) zeroed in place.
        unsafe {
            let p = libc::malloc(100) as *mut u8;
            assert!(!p.is_null());
            libc::memset(p.cast(), 0xFF, 100);

            let q = recallocarray(p.cast(), 100, 90, 1) as *mut u8;
            assert!(!q.is_null());
            // Same allocation returned (small-shrink fast path).
            assert_eq!(q, p);
            // Retained region unchanged.
            for i in 0..90 {
                assert_eq!(*q.add(i), 0xFF, "retained byte {i} changed");
            }
            // Freed tail zeroed.
            for i in 90..100 {
                assert_eq!(*q.add(i), 0, "shrunk tail byte {i} not zeroed");
            }
            libc::free(q.cast());
        }
    }

    #[test]
    fn test_recallocarray_large_shrink_copies_prefix() {
        // Shrinking by a large delta (d >= oldsize/2) takes the malloc/memcpy
        // path copying only `newsize` bytes.
        unsafe {
            let p = libc::malloc(100) as *mut u8;
            assert!(!p.is_null());
            libc::memset(p.cast(), 0x5A, 100);

            let q = recallocarray(p.cast(), 100, 10, 1) as *mut u8;
            assert!(!q.is_null());
            for i in 0..10 {
                assert_eq!(*q.add(i), 0x5A, "copied prefix byte {i} wrong");
            }
            libc::free(q.cast());
        }
    }

    #[test]
    fn test_recallocarray_overflow_returns_null() {
        // C: when the newnmemb*size multiplication would overflow, set ENOMEM
        // and return NULL (the original ptr is NOT freed in that case).
        unsafe {
            let p = libc::malloc(8);
            assert!(!p.is_null());

            crate::errno!() = 0;
            let r = recallocarray(p, 1, usize::MAX, 2);
            assert!(r.is_null(), "overflow should return NULL");
            assert_eq!(crate::errno!(), libc::ENOMEM);

            // ptr was not freed by recallocarray on the overflow path.
            libc::free(p);
        }
    }
}