//
// LDASTools frameCPP - A library implementing the LIGO/Virgo frame
// specification
//
// Copyright (C) 2026 California Institute of Technology
//
// LDASTools frameCPP is free software; you may redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools frameCPP is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

/**
 * @file test_zexpand_bounds.cc
 * @brief Regression test for issue #381 — deterministic reproducer for
 *        the ZeroSuppress `zexpand` one-word overread.
 *
 *        The 72-byte compressed buffer embedded below was extracted
 *        directly from a real H1 detector frame
 *        (H-H1_M-1470297600-3600.gwf) at the exact moment the walker
 *        SIGBUSed on macOS arm64. Instrumentation confirmed
 *        `iIn == nDataIn == 9` at the crash site — the algorithm's
 *        last-iteration two-word extraction reads `data[9]` after an
 *        `iIn++` that stepped past the 9-word (72-byte) input.
 *
 *        The test places this exact buffer flush against a PROT_NONE
 *        guard page and calls `Compression::Expand` down the
 *        `DiffExpand<8>` → `expand<INT_8U,INT_8S>` → `zexpand` code
 *        path. Prior to the safe_load fix in `zexpand`, `data[9]` here
 *        touches the guard page and the process SIGBUSes / SIGSEGVs
 *        with no diagnostic — the test runner just vanishes and no
 *        boost::test failure is recorded. With the fix, the load
 *        returns zero and Expand completes.
 */

#include <cstring>

#include <sys/mman.h>
#include <unistd.h>

#define BOOST_TEST_MODULE ZexpandBounds
#include <boost/test/included/unit_test.hpp>

#include "ldastoolsal/types.hh"
#include "framecpp/FrameCPP.hh"
#include "framecpp/Common/Compression.hh"

using namespace FrameCPP;

//---------------------------------------------------------------------------
struct FrameCPPFixture
{
    FrameCPPFixture( ) { FrameCPP::Initialize( ); }
};
BOOST_GLOBAL_FIXTURE( FrameCPPFixture );

//---------------------------------------------------------------------------
// The exact 72-byte compressed FrVect payload captured from
// H-H1_M-1470297600-3600.gwf while framecpp_verify --decompress-check
// was running against it under instrumentation. Nine INT_8U words.
// Decompresses to 60 output samples (nData=60) of INT_8 width.
//---------------------------------------------------------------------------
static const CHAR_U CRASH_BUFFER[ 72 ] = {
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x08,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x06, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
    0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff,
};
static constexpr std::size_t CRASH_BUFFER_SIZE = sizeof( CRASH_BUFFER );
static constexpr INT_8U      CRASH_NDATA       = 60;

//---------------------------------------------------------------------------
// v9 wire compression-mode value for BIGENDIAN_ZERO_SUPPRESS.
//---------------------------------------------------------------------------
namespace v9_wire
{
    constexpr INT_4U BIGENDIAN_ZERO_SUPPRESS = 6;
}

static const Compression::compress_type_mapping&
compress_map( )
{
    static Compression::compress_type_mapping m;
    if ( m.empty( ) )
    {
        m[ v9_wire::BIGENDIAN_ZERO_SUPPRESS ] =
            Compression::MODE_ZERO_SUPPRESS;
    }
    return m;
}

static const Compression::data_type_mapping&
data_map( )
{
    static Compression::data_type_mapping m;
    if ( m.empty( ) )
    {
        // The crash buffer is 8-byte-typed; the Expand dispatcher routes
        // that to DiffExpand<8> which instantiates zexpand<INT_8S,INT_8U>.
        m[ Compression::FR_VECT_8S ] = Compression::FR_VECT_8S;
    }
    return m;
}

//---------------------------------------------------------------------------
// RAII wrapper: one writable page followed by one PROT_NONE guard page.
// Any read past `end()` faults.
//---------------------------------------------------------------------------
class GuardedPage
{
public:
    GuardedPage( )
        : m_page_size( static_cast< std::size_t >(
              ::sysconf( _SC_PAGESIZE ) ) ),
          m_base( nullptr )
    {
        void* mem = ::mmap( nullptr,
                            2 * m_page_size,
                            PROT_READ | PROT_WRITE,
                            MAP_ANON | MAP_PRIVATE,
                            -1,
                            0 );
        BOOST_REQUIRE( mem != MAP_FAILED );
        m_base = static_cast< CHAR_U* >( mem );
        BOOST_REQUIRE_EQUAL(
            ::mprotect( m_base + m_page_size, m_page_size, PROT_NONE ),
            0 );
    }

    ~GuardedPage( )
    {
        if ( m_base )
            ::munmap( m_base, 2 * m_page_size );
    }

    GuardedPage( const GuardedPage& )            = delete;
    GuardedPage& operator=( const GuardedPage& ) = delete;

    CHAR_U*     end( ) const { return m_base + m_page_size; }
    std::size_t page_size( ) const { return m_page_size; }

private:
    std::size_t m_page_size;
    CHAR_U*     m_base;
};

//---------------------------------------------------------------------------
BOOST_AUTO_TEST_CASE( zexpand_no_overread_past_guard_page )
{
    // The captured crash buffer must fit within one page (it does, at
    // 72 bytes).
    GuardedPage page;
    BOOST_REQUIRE_LE( CRASH_BUFFER_SIZE, page.page_size( ) );

    // Position the compressed buffer so its last byte sits exactly one
    // byte before the guard page. Any read past end-of-buffer touches
    // PROT_NONE memory.
    CHAR_U* src = page.end( ) - CRASH_BUFFER_SIZE;
    std::memcpy( src, CRASH_BUFFER, CRASH_BUFFER_SIZE );

    BOOST_REQUIRE_EQUAL(
        static_cast< void* >( src + CRASH_BUFFER_SIZE ),
        static_cast< void* >( page.end( ) ) );

    // Call Expand down the 8-byte ZERO_SUPPRESS path. Prior to the
    // safe_load fix in zexpand, the algorithm's last-iteration
    // two-word extraction reads data[9] after `iIn++` steps past the
    // 9-word input buffer — that read now lands on the guard page and
    // SIGBUSes/SIGSEGVs the process. With the fix, safe_load
    // substitutes zero and Expand completes.
    Compression::output_type expanded;
    INT_8U                   expanded_bytes = 0;

    BOOST_REQUIRE_NO_THROW( Compression::Expand(
        /*NativeOrder=*/true,
        /*CompressionMode=*/v9_wire::BIGENDIAN_ZERO_SUPPRESS,
        compress_map( ),
        /*DataType=*/Compression::FR_VECT_8S,
        data_map( ),
        src,
        /*SourceLength=*/CRASH_NDATA,
        /*SourceByteLength=*/CRASH_BUFFER_SIZE,
        expanded,
        expanded_bytes ) );

    // Successful expansion produces CRASH_NDATA output samples of the
    // 8-byte type.
    BOOST_REQUIRE( expanded );
    BOOST_CHECK_EQUAL( expanded_bytes, CRASH_NDATA * sizeof( INT_8S ) );
}
