// Copyright (C) 2020-2023 Free Software Foundation, Inc.
// This file is part of GCC.
// GCC is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3, or (at your option) any later
// version.
// GCC 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 GCC; see the file COPYING3.  If not see
// <http://www.gnu.org/licenses/>.
#include "rust-system.h"
#include "rust-diagnostics.h"
#include "rust-imports.h"
#include "rust-object-export.h"
#include "rust-export-metadata.h"
#ifndef O_BINARY
#define O_BINARY 0
#endif
namespace Rust {
// The list of paths we search for import files.
static std::vector<std::string> search_path;
// Add a directory to the search path.  This is called from the option
// handling language hook.
void
add_search_path (const std::string &path)
{
  search_path.push_back (path);
}
// Find import data.  This searches the file system for FILENAME and
// returns a pointer to a Stream object to read the data that it
// exports.  If the file is not found, it returns NULL.
// When FILENAME is not an absolute path and does not start with ./ or
// ../, we use the search path provided by -I and -L options.
// When FILENAME does start with ./ or ../, we use
// RELATIVE_IMPORT_PATH as a prefix.
// When FILENAME does not exist, we try modifying FILENAME to find the
// file.  We use the first of these which exists:
//   * We append ".gox".
//   * We turn the base of FILENAME into libFILENAME.so.
//   * We turn the base of FILENAME into libFILENAME.a.
//   * We append ".o".
// When using a search path, we apply each of these transformations at
// each entry on the search path before moving on to the next entry.
// If the file exists, but does not contain any Go export data, we
// stop; we do not keep looking for another file with the same name
// later in the search path.
Import::Stream *
Import::open_package (const std::string &filename, Location location,
		      const std::string &relative_import_path)
{
  bool is_local;
  if (IS_ABSOLUTE_PATH (filename))
    is_local = true;
  else if (filename[0] == '.'
	   && (filename[1] == '\0' || IS_DIR_SEPARATOR (filename[1])))
    is_local = true;
  else if (filename[0] == '.' && filename[1] == '.'
	   && (filename[2] == '\0' || IS_DIR_SEPARATOR (filename[2])))
    is_local = true;
  else
    is_local = false;
  std::string fn = filename;
  if (is_local && !IS_ABSOLUTE_PATH (filename)
      && !relative_import_path.empty ())
    {
      if (fn == ".")
	{
	  // A special case.
	  fn = relative_import_path;
	}
      else if (fn[0] == '.' && fn[1] == '.'
	       && (fn[2] == '\0' || IS_DIR_SEPARATOR (fn[2])))
	{
	  // We are going to join relative_import_path and fn, and it
	  // will look like DIR/../PATH.  But DIR does not necessarily
	  // exist in this case, and if it doesn't the use of .. will
	  // fail although it shouldn't.  The gc compiler uses
	  // path.Join here, which cleans up the .., so we need to do
	  // the same.
	  size_t index;
	  for (index = relative_import_path.length () - 1;
	       index > 0 && !IS_DIR_SEPARATOR (relative_import_path[index]);
	       index--)
	    ;
	  if (index > 0)
	    fn = relative_import_path.substr (0, index) + fn.substr (2);
	  else
	    fn = relative_import_path + '/' + fn;
	}
      else
	fn = relative_import_path + '/' + fn;
      is_local = false;
    }
  if (!is_local)
    {
      for (std::vector<std::string>::const_iterator p = search_path.begin ();
	   p != search_path.end (); ++p)
	{
	  std::string indir = *p;
	  if (!indir.empty () && indir[indir.size () - 1] != '/')
	    indir += '/';
	  indir += fn;
	  Stream *s = Import::try_package_in_directory (indir, location);
	  if (s != NULL)
	    return s;
	}
    }
  Stream *s = Import::try_package_in_directory (fn, location);
  if (s != NULL)
    return s;
  return NULL;
}
// Try to find the export data for FILENAME.
Import::Stream *
Import::try_package_in_directory (const std::string &filename,
				  Location location)
{
  std::string found_filename = filename;
  int fd = open (found_filename.c_str (), O_RDONLY | O_BINARY);
  if (fd >= 0)
    {
      struct stat s;
      if (fstat (fd, &s) >= 0 && S_ISDIR (s.st_mode))
	{
	  close (fd);
	  fd = -1;
	  errno = EISDIR;
	}
    }
  if (fd < 0)
    {
      if (errno != ENOENT && errno != EISDIR)
	rust_warning_at (location, 0, "%s: %m", filename.c_str ());
      fd = Import::try_suffixes (&found_filename);
      if (fd < 0)
	return NULL;
    }
  // The export data may not be in this file.
  Stream *s = Import::find_export_data (found_filename, fd, location);
  if (s != NULL)
    return s;
  close (fd);
  rust_error_at (location, "%s exists but does not contain any Go export data",
		 found_filename.c_str ());
  return NULL;
}
// Given import "*PFILENAME", where *PFILENAME does not exist, try
// various suffixes.  If we find one, set *PFILENAME to the one we
// found.  Return the open file descriptor.
int
Import::try_suffixes (std::string *pfilename)
{
  std::string filename = *pfilename + ".rox";
  int fd = open (filename.c_str (), O_RDONLY | O_BINARY);
  if (fd >= 0)
    {
      *pfilename = filename;
      return fd;
    }
  const char *basename = lbasename (pfilename->c_str ());
  size_t basename_pos = basename - pfilename->c_str ();
  filename = pfilename->substr (0, basename_pos) + "lib" + basename + ".so";
  fd = open (filename.c_str (), O_RDONLY | O_BINARY);
  if (fd >= 0)
    {
      *pfilename = filename;
      return fd;
    }
  filename = pfilename->substr (0, basename_pos) + "lib" + basename + ".a";
  fd = open (filename.c_str (), O_RDONLY | O_BINARY);
  if (fd >= 0)
    {
      *pfilename = filename;
      return fd;
    }
  filename = *pfilename + ".o";
  fd = open (filename.c_str (), O_RDONLY | O_BINARY);
  if (fd >= 0)
    {
      *pfilename = filename;
      return fd;
    }
  return -1;
}
// Look for export data in the file descriptor FD.
Import::Stream *
Import::find_export_data (const std::string &filename, int fd,
			  Location location)
{
  // See if we can read this as an object file.
  Import::Stream *stream
    = Import::find_object_export_data (filename, fd, 0, location);
  if (stream != NULL)
    return stream;
  const int len = sizeof (Metadata::kMagicHeader);
  if (lseek (fd, 0, SEEK_SET) < 0)
    {
      rust_error_at (location, "lseek %s failed: %m", filename.c_str ());
      return NULL;
    }
  char buf[len];
  ssize_t c = ::read (fd, buf, len);
  if (c < len)
    return NULL;
  // Check for a file containing nothing but Go export data.
  // if (memcmp (buf, Export::cur_magic, Export::magic_len) == 0
  //     || memcmp (buf, Export::v1_magic, Export::magic_len) == 0
  //     || memcmp (buf, Export::v2_magic, Export::magic_len) == 0)
  //
  // FIXME we need to work out a better header
  //
  if (memcmp (buf, Metadata::kMagicHeader, sizeof (Metadata::kMagicHeader))
      == 0)
    return new Stream_from_file (fd);
  // See if we can read this as an archive.
  if (Import::is_archive_magic (buf))
    return Import::find_archive_export_data (filename, fd, location);
  return NULL;
}
// Look for export data in an object file.
Import::Stream *
Import::find_object_export_data (const std::string &filename, int fd,
				 off_t offset, Location location)
{
  char *buf;
  size_t len;
  int err;
  const char *errmsg = rust_read_export_data (fd, offset, &buf, &len, &err);
  if (errmsg != NULL)
    {
      if (err == 0)
	rust_error_at (location, "%s: %s", filename.c_str (), errmsg);
      else
	rust_error_at (location, "%s: %s: %s", filename.c_str (), errmsg,
		       xstrerror (err));
      return NULL;
    }
  if (buf == NULL)
    return NULL;
  return new Stream_from_buffer (buf, len);
}
// Class Import.
// Construct an Import object.  We make the builtin_types_ vector
// large enough to hold all the builtin types.
Import::Import (Stream *stream, Location location)
  : stream_ (stream), location_ (location)
{}
// Import the data in the associated stream.
// Read LENGTH bytes from the stream.
void
Import::read (size_t length, std::string *out)
{
  const char *data;
  if (!this->stream_->peek (length, &data))
    {
      if (!this->stream_->saw_error ())
	rust_error_at (this->location_, "import error at %d: expected %d bytes",
		       this->stream_->pos (), static_cast<int> (length));
      this->stream_->set_saw_error ();
      *out = std::string ("");
      return;
    }
  *out = std::string (data, length);
  this->advance (length);
}
// Class Import::Stream.
Import::Stream::Stream () : pos_ (0), saw_error_ (false) {}
Import::Stream::~Stream () {}
// Return the next character to come from the stream.
int
Import::Stream::peek_char ()
{
  const char *read;
  if (!this->do_peek (1, &read))
    return -1;
  // Make sure we return an unsigned char, so that we don't get
  // confused by \xff.
  unsigned char ret = *read;
  return ret;
}
// Return true if the next LENGTH characters from the stream match
// BYTES
bool
Import::Stream::match_bytes (const char *bytes, size_t length)
{
  const char *read;
  if (!this->do_peek (length, &read))
    return false;
  return memcmp (bytes, read, length) == 0;
}
// Require that the next LENGTH bytes from the stream match BYTES.
void
Import::Stream::require_bytes (Location location, const char *bytes,
			       size_t length)
{
  const char *read;
  if (!this->do_peek (length, &read) || memcmp (bytes, read, length) != 0)
    {
      if (!this->saw_error_)
	rust_error_at (location, "import error at %d: expected %<%.*s%>",
		       this->pos (), static_cast<int> (length), bytes);
      this->saw_error_ = true;
      return;
    }
  this->advance (length);
}
// Class Stream_from_file.
Stream_from_file::Stream_from_file (int fd) : fd_ (fd), data_ ()
{
  if (lseek (fd, 0, SEEK_SET) != 0)
    {
      rust_fatal_error (Linemap::unknown_location (), "lseek failed: %m");
      this->set_saw_error ();
    }
}
Stream_from_file::~Stream_from_file () { close (this->fd_); }
// Read next bytes.
bool
Stream_from_file::do_peek (size_t length, const char **bytes)
{
  if (this->data_.length () >= length)
    {
      *bytes = this->data_.data ();
      return true;
    }
  this->data_.resize (length);
  ssize_t got = ::read (this->fd_, &this->data_[0], length);
  if (got < 0)
    {
      if (!this->saw_error ())
	rust_fatal_error (Linemap::unknown_location (), "read failed: %m");
      this->set_saw_error ();
      return false;
    }
  if (lseek (this->fd_, -got, SEEK_CUR) < 0)
    {
      if (!this->saw_error ())
	rust_fatal_error (Linemap::unknown_location (), "lseek failed: %m");
      this->set_saw_error ();
      return false;
    }
  if (static_cast<size_t> (got) < length)
    return false;
  *bytes = this->data_.data ();
  return true;
}
// Advance.
void
Stream_from_file::do_advance (size_t skip)
{
  if (lseek (this->fd_, skip, SEEK_CUR) < 0)
    {
      if (!this->saw_error ())
	rust_fatal_error (Linemap::unknown_location (), "lseek failed: %m");
      this->set_saw_error ();
    }
  if (!this->data_.empty ())
    {
      if (this->data_.length () > skip)
	this->data_.erase (0, skip);
      else
	this->data_.clear ();
    }
}
} // namespace Rust