//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2017 OpenVPN Technologies, Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License Version 3
//    as published by the Free Software Foundation.
//
//    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 in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

// Base class and factory for compression/decompression objects.
// Currently we support LZO, Snappy, and LZ4 implementations.

#ifndef OPENVPN_COMPRESS_COMPRESS_H
#define OPENVPN_COMPRESS_COMPRESS_H

#include <openvpn/common/size.hpp>
#include <openvpn/common/exception.hpp>
#include <openvpn/common/rc.hpp>
#include <openvpn/common/likely.hpp>
#include <openvpn/buffer/buffer.hpp>
#include <openvpn/frame/frame.hpp>
#include <openvpn/log/sessionstats.hpp>

#define OPENVPN_LOG_COMPRESS(x)
#define OPENVPN_LOG_COMPRESS_VERBOSE(x)

#if defined(OPENVPN_DEBUG_COMPRESS)
#if OPENVPN_DEBUG_COMPRESS >= 1
#undef OPENVPN_LOG_COMPRESS
#define OPENVPN_LOG_COMPRESS(x) OPENVPN_LOG(x)
#endif
#if OPENVPN_DEBUG_COMPRESS >= 2
#undef OPENVPN_LOG_COMPRESS_VERBOSE
#define OPENVPN_LOG_COMPRESS_VERBOSE(x) OPENVPN_LOG(x)
#endif
#endif

namespace openvpn {
  class Compress : public RC<thread_unsafe_refcount>
  {
  public:
    typedef RCPtr<Compress> Ptr;

    // Compressor name
    virtual const char *name() const = 0;

    // Compression method implemented by underlying compression class.
    // hint should normally be true to compress the data.  If hint is
    // false, the data may be uncompressible or already compressed,
    // so method shouldn't attempt compression.
    virtual void compress(BufferAllocated& buf, const bool hint) = 0;

    // Decompression method implemented by underlying compression class.
    virtual void decompress(BufferAllocated& buf) = 0;

  protected:
    // magic numbers to indicate no compression
    enum {
      NO_COMPRESS      = 0xFA,
      NO_COMPRESS_SWAP = 0xFB, // for better alignment handling, replace this byte with last byte of packet
    };

    // Compress V2 constants
    enum {
      COMPRESS_V2_ESCAPE=0x50,

      // Compression algs
      OVPN_COMPv2_NONE=0,
      OVPN_COMPv2_LZ4=1,
    };

    Compress(const Frame::Ptr& frame_arg,
	     const SessionStats::Ptr& stats_arg)
      : frame(frame_arg), stats(stats_arg) {}

    void error(BufferAllocated& buf)
    {
      stats->error(Error::COMPRESS_ERROR);
      buf.reset_size();
    }

    void do_swap(Buffer& buf, unsigned char op)
    {
      if (buf.size())
	{
	  buf.push_back(buf[0]);
	  buf[0] = op;
	}
      else
	buf.push_back(op);
    }

    void do_unswap(Buffer& buf)
    {
      if (buf.size() >= 2)
	{
	  const unsigned char first = buf.pop_back();
	  buf.push_front(first);
	}
    }

    // Push a COMPRESS_V2 header byte (value).
    // Pass value == 0 to omit push.
    void v2_push(Buffer& buf, int value)
    {
      unsigned char uc = buf[0];
      if (value == 0 && uc != COMPRESS_V2_ESCAPE)
	return;
      unsigned char *esc = buf.prepend_alloc(2);
      esc[0] = COMPRESS_V2_ESCAPE;
      esc[1] = value;
    }

    // Pull a COMPRESS_V2 header byte.
    // Returns the compress op (> 0) on success.
    // Returns 0 if no compress op.
    int v2_pull(Buffer& buf)
    {
      unsigned char uc = buf[0];
      if (uc != COMPRESS_V2_ESCAPE)
	return 0;
      uc = buf[1];
      buf.advance(2);
      return uc;
    }

    Frame::Ptr frame;
    SessionStats::Ptr stats;
  };
}

// include compressor implementations here
#include <openvpn/compress/compnull.hpp>
#include <openvpn/compress/compstub.hpp>

#ifndef NO_LZO
#include <openvpn/compress/lzoselect.hpp>
#endif
#ifdef HAVE_LZ4
#include <openvpn/compress/lz4.hpp>
#endif
#ifdef HAVE_SNAPPY
#include <openvpn/compress/snappy.hpp>
#endif

namespace openvpn {
  class CompressContext
  {
  public:
    enum Type {
      NONE,
      COMP_STUB,  // generic compression stub
      COMP_STUBv2,  // generic compression stub using v2 protocol
      ANY,        // placeholder for any method on client, before server assigns it
      ANY_LZO,    // placeholder for LZO or LZO_STUB methods on client, before server assigns it
      LZO,
      LZO_SWAP,
      LZO_STUB,
      LZ4,
      LZ4v2,
      SNAPPY,
    };

    OPENVPN_SIMPLE_EXCEPTION(compressor_unavailable);

    CompressContext() : type_(NONE) {}

    explicit CompressContext(const Type t, const bool asym)
      : asym_(asym) // asym indicates asymmetrical compression where only downlink is compressed
    {
      if (!compressor_available(t))
	throw compressor_unavailable();
      type_ = t;
    }

    Type type() const { return type_; }
    bool asym() const { return asym_; }

    unsigned int extra_payload_bytes() const
    {
      switch (type_)
	{
	case NONE:
	  return 0;
	case COMP_STUBv2:
	case LZ4v2:
	  return 2; // worst case
	default:
	  return 1;
	}
    }

    Compress::Ptr new_compressor(const Frame::Ptr& frame, const SessionStats::Ptr& stats)
    {
      switch (type_)
	{
	case NONE:
	  return new CompressNull(frame, stats);
	case ANY:
	case ANY_LZO:
	case LZO_STUB:
	  return new CompressStub(frame, stats, false);
	case COMP_STUB:
	  return new CompressStub(frame, stats, true);
	case COMP_STUBv2:
	  return new CompressStubV2(frame, stats);
#ifndef NO_LZO
	case LZO:
	  return new CompressLZO(frame, stats, false, asym_);
	case LZO_SWAP:
	  return new CompressLZO(frame, stats, true, asym_);
#endif
#ifdef HAVE_LZ4
	case LZ4:
	  return new CompressLZ4(frame, stats, asym_);
	case LZ4v2:
	  return new CompressLZ4v2(frame, stats, asym_);
#endif
#ifdef HAVE_SNAPPY
	case SNAPPY:
	  return new CompressSnappy(frame, stats, asym_);
#endif
	default:
	  throw compressor_unavailable();
	}
    }

    static bool compressor_available(const Type t)
    {
      switch (t)
	{
	case NONE:
	case ANY:
	case ANY_LZO:
	case LZO_STUB:
	case COMP_STUB:
	case COMP_STUBv2:
	  return true;
	case LZO:
	case LZO_SWAP:
#ifndef NO_LZO
	  return true;
#else
	  return false;
#endif
	case LZ4:
#ifdef HAVE_LZ4
	  return true;
#else
	  return false;
#endif
	case LZ4v2:
#ifdef HAVE_LZ4
	  return true;
#else
	  return false;
#endif
	case SNAPPY:
#ifdef HAVE_SNAPPY
	  return true;
#else
	  return false;
#endif
	default:
	  return false;
	}
    }

    // On the client, used to tell server which compression methods we support.
    // Includes compression V1 and V2 methods.
    const char *peer_info_string() const
    {
      switch (type_)
	{
#ifndef NO_LZO
	case LZO:
	  return "IV_LZO=1\n";
	case LZO_SWAP:
	  return "IV_LZO_SWAP=1\n";
#endif
#ifdef HAVE_LZ4
	case LZ4:
	  return "IV_LZ4=1\n";
#endif
#ifdef HAVE_LZ4
	case LZ4v2:
	  return "IV_LZ4v2=1\n";
#endif
#ifdef HAVE_SNAPPY
	case SNAPPY:
	  return "IV_SNAPPY=1\n";
#endif
	case LZO_STUB:
	case COMP_STUB:
	case COMP_STUBv2:
	  return
	    "IV_LZO_STUB=1\n"
	    "IV_COMP_STUB=1\n"
	    "IV_COMP_STUBv2=1\n"
	    ;
	case ANY:
	  return
#ifdef HAVE_SNAPPY
	    "IV_SNAPPY=1\n"
#endif
#ifndef NO_LZO
	    "IV_LZO=1\n"
	    "IV_LZO_SWAP=1\n"
#else
	    "IV_LZO_STUB=1\n"
#endif
#ifdef HAVE_LZ4
	    "IV_LZ4=1\n"
	    "IV_LZ4v2=1\n"
#endif
	    "IV_COMP_STUB=1\n"
	    "IV_COMP_STUBv2=1\n"
	    ;
	case ANY_LZO:
	  return
#ifndef NO_LZO
	    "IV_LZO=1\n"
	    "IV_LZO_SWAP=1\n"
#else
	    "IV_LZO_STUB=1\n"
#endif
	    "IV_COMP_STUB=1\n"
	    "IV_COMP_STUBv2=1\n"
	    ;
	default:
	  return nullptr;
	}
    }

    // On the client, used to tell server which compression methods we support.
    // Limited only to compression V1 methods.
    const char *peer_info_string_v1() const
    {
      switch (type_)
	{
#ifndef NO_LZO
	case LZO:
	  return "IV_LZO=1\n";
	case LZO_SWAP:
	  return "IV_LZO_SWAP=1\n";
#endif
#ifdef HAVE_LZ4
	case LZ4:
	  return "IV_LZ4=1\n";
#endif
#ifdef HAVE_SNAPPY
	case SNAPPY:
	  return "IV_SNAPPY=1\n";
#endif
	case LZO_STUB:
	case COMP_STUB:
	  return
	    "IV_LZO_STUB=1\n"
	    "IV_COMP_STUB=1\n"
	    ;
	case ANY:
	  return
#ifdef HAVE_SNAPPY
	    "IV_SNAPPY=1\n"
#endif
#ifndef NO_LZO
	    "IV_LZO=1\n"
	    "IV_LZO_SWAP=1\n"
#else
	    "IV_LZO_STUB=1\n"
#endif
#ifdef HAVE_LZ4
	    "IV_LZ4=1\n"
#endif
	    "IV_COMP_STUB=1\n"
	    ;
	case ANY_LZO:
	  return
#ifndef NO_LZO
	    "IV_LZO=1\n"
	    "IV_LZO_SWAP=1\n"
#else
	    "IV_LZO_STUB=1\n"
#endif
	    "IV_COMP_STUB=1\n"
	    ;
	default:
	  return nullptr;
	}
    }

    const char *options_string() const
    {
      switch (type_)
	{
	case LZO:
	case LZO_STUB:
	case SNAPPY:
	case LZ4:
	case LZ4v2:
	case LZO_SWAP:
	case COMP_STUB:
	case COMP_STUBv2:
	case ANY:
	case ANY_LZO:
	  return "comp-lzo";
	default:
	  return nullptr;
	}
    }

    const char *str() const
    {
      switch (type_)
	{
	case LZO:
	  return "LZO";
	case LZO_SWAP:
	  return "LZO_SWAP";
	case LZ4:
	  return "LZ4";
	case LZ4v2:
	  return "LZ4v2";
	case SNAPPY:
	  return "SNAPPY";
	case LZO_STUB:
	  return "LZO_STUB";
	case COMP_STUB:
	  return "COMP_STUB";
	case COMP_STUBv2:
	  return "COMP_STUBv2";
	case ANY:
	  return "ANY";
	case ANY_LZO:
	  return "ANY_LZO";
	default:
	  return "NONE";
	}
    }

    static Type parse_method(const std::string& method)
    {
      if (method == "lzo")
	return LZO;
      else if (method == "lzo-swap")
	return LZO_SWAP;
      else if (method == "lzo-stub")
	return LZO_STUB;
      else if (method == "lz4")
	return LZ4;
      else if (method == "lz4-v2")
	return LZ4v2;
      else if (method == "snappy")
	return SNAPPY;
      else if (method == "stub")
	return COMP_STUB;
      else if (method == "stub-v2")
	return COMP_STUBv2;
      else
	return NONE;
    }

    static Type stub(const Type t)
    {
      switch (t)
	{
	case COMP_STUBv2:
	case LZ4v2:
	  return COMP_STUBv2;
	default:
	  return COMP_STUB;
	}
    }

    static void init_static()
    {
#ifndef NO_LZO
      CompressLZO::init_static();
#endif
    }

  private:
    Type type_;
    bool asym_;
  };

} // namespace openvpn

#endif // OPENVPN_COMPRESS_COMPRESS_H
