// Copyright 2022 The Centipede Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "./centipede/shared_memory_blob_sequence.h"

#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <cstdint>
#include <cstdio>

namespace centipede {

static void ErrorOnFailure(bool condition, const char *text) {
  if (!condition) return;
  std::perror(text);
  abort();
}

BlobSequence::BlobSequence(uint8_t *data, size_t size)
    : data_(data), size_(size) {
  ErrorOnFailure(size < sizeof(Blob::size), "Size too small");
}

bool BlobSequence::Write(Blob blob) {
  ErrorOnFailure(!blob.IsValid(), "Write(): blob.tag must not be zero");
  ErrorOnFailure(had_reads_after_reset_, "Write(): Had reads after reset");
  had_writes_after_reset_ = true;
  if (offset_ + sizeof(blob.size) + sizeof(blob.tag) + blob.size > size_)
    return false;

  // Write tag.
  memcpy(data_ + offset_, &blob.tag, sizeof(blob.tag));
  offset_ += sizeof(blob.tag);

  // Write size.
  memcpy(data_ + offset_, &blob.size, sizeof(blob.size));
  offset_ += sizeof(blob.size);
  // Write data.
  memcpy(data_ + offset_, blob.data, blob.size);
  offset_ += blob.size;
  if (offset_ + sizeof(blob.size) + sizeof(blob.tag) <= size_) {
    // Write zero tag/size to data_+offset_ but don't change the offset.
    // This is required to overwrite any stale bits in data_.
    Blob invalid_blob;  // invalid.
    memcpy(data_ + offset_, &invalid_blob.tag, sizeof(invalid_blob.tag));
    memcpy(data_ + offset_ + sizeof(invalid_blob.tag), &invalid_blob.size,
           sizeof(invalid_blob.size));
  }
  return true;
}

Blob BlobSequence::Read() {
  ErrorOnFailure(had_writes_after_reset_, "Had writes after reset");
  had_reads_after_reset_ = true;
  if (offset_ + sizeof(Blob::size) + sizeof(Blob::tag) > size_) return {};
  // Read blob_tag.
  Blob::SizeAndTagT blob_tag = 0;
  memcpy(&blob_tag, data_ + offset_, sizeof(blob_tag));
  offset_ += sizeof(blob_tag);
  // Read blob_size.
  Blob::SizeAndTagT blob_size = 0;
  memcpy(&blob_size, data_ + offset_, sizeof(Blob::size));
  offset_ += sizeof(Blob::size);
  // Read blob_data.
  ErrorOnFailure(offset_ + blob_size > size_, "Not enough bytes");
  if (blob_tag == 0 && blob_size == 0) return {};
  ErrorOnFailure(blob_tag == 0, "Read: blob.tag must not be zero");
  Blob result{blob_tag, blob_size, data_ + offset_};
  offset_ += result.size;
  return result;
}

void BlobSequence::Reset() {
  offset_ = 0;
  had_reads_after_reset_ = false;
  had_writes_after_reset_ = false;
}

SharedMemoryBlobSequence::SharedMemoryBlobSequence(const char *name,
                                                   size_t size,
                                                   bool use_posix_shmem) {
  ErrorOnFailure(size < sizeof(Blob::size), "Size too small");
  size_ = size;
  if (use_posix_shmem) {
    fd_ = shm_open(name, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    ErrorOnFailure(fd_ < 0, "shm_open() failed");
    strncpy(path_, name, PATH_MAX);
    ErrorOnFailure(path_[PATH_MAX - 1] != 0,
                   "shm_open() path length exceeds PATH_MAX.");
    path_is_owned_ = true;
  } else {
    fd_ = memfd_create(name, MFD_CLOEXEC);
    ErrorOnFailure(fd_ < 0, "memfd_create() failed");
    const size_t path_size =
        snprintf(path_, PATH_MAX, "/proc/%d/fd/%d", getpid(), fd_);
    ErrorOnFailure(path_size >= PATH_MAX,
                   "internal fd path length exceeds PATH_MAX.");
    // memfd_create descriptors are automatically freed on close().
    path_is_owned_ = false;
  }
  ErrorOnFailure(ftruncate(fd_, static_cast<__off_t>(size_)),
                 "ftruncate() failed)");
  MmapData();
}

SharedMemoryBlobSequence::SharedMemoryBlobSequence(const char *path) {
  // This is a quick way to tell shm-allocated paths from memfd paths without
  // requiring the caller to specify.
  if (strncmp(path, "/proc/", 6) == 0) {
    fd_ = open(path, O_RDWR, O_CLOEXEC);
  } else {
    fd_ = shm_open(path, O_RDWR, 0);
  }
  ErrorOnFailure(fd_ < 0, "open() failed");
  strncpy(path_, path, PATH_MAX);
  ErrorOnFailure(path_[PATH_MAX - 1] != 0, "path length exceeds PATH_MAX.");
  struct stat statbuf = {};
  ErrorOnFailure(fstat(fd_, &statbuf), "fstat() failed");
  size_ = statbuf.st_size;
  MmapData();
}

void SharedMemoryBlobSequence::MmapData() {
  data_ = static_cast<uint8_t *>(
      mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
  ErrorOnFailure(data_ == MAP_FAILED, "mmap() failed");
}

SharedMemoryBlobSequence::~SharedMemoryBlobSequence() {
  if (path_is_owned_) {
    ErrorOnFailure(shm_unlink(path_), "shm_unlink() failed");
  }
  ErrorOnFailure(munmap(data_, size_), "munmap() failed");
  ErrorOnFailure(close(fd_), "close() failed");
}

void SharedMemoryBlobSequence::ReleaseSharedMemory() {
  // Setting size to 0 releases the memory to OS.
  ErrorOnFailure(ftruncate(fd_, 0) != 0, "ftruncate(0) failed)");
  // Set the size back to `size`. The memory is not actually reserved.
  ErrorOnFailure(ftruncate(fd_, size_) != 0, "ftruncate(size_) failed)");
}

size_t SharedMemoryBlobSequence::NumBytesUsed() const {
  struct stat statbuf;
  ErrorOnFailure(fstat(fd_, &statbuf), "fstat() failed)");
  return statbuf.st_blocks * S_BLKSIZE;
}

}  // namespace centipede
