// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "services/device/serial/serial_device_enumerator_linux.h"

#include <stdint.h>

#include <memory>
#include <string_view>
#include <utility>
#include <vector>

#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/files/file_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/threading/scoped_blocking_call.h"
#include "components/device_event_log/device_event_log.h"
#include "device/udev_linux/udev.h"

namespace device {

namespace {

// Holds information about a TTY driver for serial devices. Each driver creates
// device nodes with a given major number and in a range of minor numbers.
struct SerialDriverInfo {
  int major;
  int minor_start;
  int minor_end;  // Inclusive.
};

std::vector<SerialDriverInfo> ReadSerialDriverInfo(const base::FilePath& path) {
  std::string tty_drivers;
  if (!base::ReadFileToString(path, &tty_drivers)) {
    return {};
  }

  // Each line has information on a single TTY driver.
  std::vector<SerialDriverInfo> serial_drivers;
  for (const auto& line :
       base::SplitStringPiece(tty_drivers, "\n", base::KEEP_WHITESPACE,
                              base::SPLIT_WANT_NONEMPTY)) {
    std::vector<std::string_view> fields = base::SplitStringPiece(
        line, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

    // The format of each line is:
    //
    //   driver name<SPACE>name<SPACE>major<SPACE>minor range<SPACE>type
    //
    // We only care about drivers that provide the "serial" type. The rest are
    // things like pseudoterminals.
    if (fields.size() < 5 || fields[4] != "serial")
      continue;

    SerialDriverInfo info;
    if (!base::StringToInt(fields[2], &info.major))
      continue;

    std::vector<std::string_view> minor_range = base::SplitStringPiece(
        fields[3], "-", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
    if (minor_range.size() == 1) {
      if (!base::StringToInt(minor_range[0], &info.minor_start))
        continue;
      info.minor_end = info.minor_start;
    } else if (minor_range.size() == 2) {
      if (!base::StringToInt(minor_range[0], &info.minor_start) ||
          !base::StringToInt(minor_range[1], &info.minor_end)) {
        continue;
      }
    } else {
      continue;
    }

    serial_drivers.push_back(info);
  }

  return serial_drivers;
}

}  // namespace

// static
std::unique_ptr<SerialDeviceEnumeratorLinux>
SerialDeviceEnumeratorLinux::Create() {
  return std::make_unique<SerialDeviceEnumeratorLinux>(
      base::FilePath("/proc/tty/drivers"));
}

SerialDeviceEnumeratorLinux::SerialDeviceEnumeratorLinux(
    const base::FilePath& tty_driver_info_path)
    : tty_driver_info_path_(tty_driver_info_path) {
  DETACH_FROM_SEQUENCE(sequence_checker_);

  watcher_ = UdevWatcher::StartWatching(this);
  if (watcher_)
    watcher_->EnumerateExistingDevices();
}

SerialDeviceEnumeratorLinux::~SerialDeviceEnumeratorLinux() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void SerialDeviceEnumeratorLinux::OnDeviceAdded(ScopedUdevDevicePtr device) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  const char* subsystem = udev_device_get_subsystem(device.get());
  if (!subsystem || UNSAFE_TODO(strcmp(subsystem, "tty")) != 0) {
    return;
  }

  const char* syspath_str = udev_device_get_syspath(device.get());
  if (!syspath_str)
    return;
  std::string syspath(syspath_str);

  const char* major_str = udev_device_get_property_value(device.get(), "MAJOR");
  const char* minor_str = udev_device_get_property_value(device.get(), "MINOR");

  int major, minor;
  if (!major_str || !minor_str || !base::StringToInt(major_str, &major) ||
      !base::StringToInt(minor_str, &minor)) {
    return;
  }

  for (const auto& driver : ReadSerialDriverInfo(tty_driver_info_path_)) {
    if (major == driver.major && minor >= driver.minor_start &&
        minor <= driver.minor_end) {
      CreatePort(std::move(device), syspath);
      return;
    }
  }
}

void SerialDeviceEnumeratorLinux::OnDeviceChanged(ScopedUdevDevicePtr device) {}

void SerialDeviceEnumeratorLinux::OnDeviceRemoved(ScopedUdevDevicePtr device) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  const char* syspath = udev_device_get_syspath(device.get());
  if (!syspath)
    return;

  auto it = paths_.find(syspath);
  if (it == paths_.end())
    return;
  base::UnguessableToken token = it->second;

  paths_.erase(it);
  RemovePort(token);
}

void SerialDeviceEnumeratorLinux::CreatePort(ScopedUdevDevicePtr device,
                                             const std::string& syspath) {
  const char* path = udev_device_get_property_value(device.get(), "DEVNAME");
  if (!path)
    return;

  auto token = base::UnguessableToken::Create();
  auto info = mojom::SerialPortInfo::New();
  info->path = base::FilePath(path);
  info->token = token;

  uint32_t int_value;
  const char* vendor_id =
      udev_device_get_property_value(device.get(), "ID_VENDOR_ID");
  if (vendor_id && base::HexStringToUInt(vendor_id, &int_value)) {
    info->vendor_id = int_value;
    info->has_vendor_id = true;
  }

  const char* product_id =
      udev_device_get_property_value(device.get(), "ID_MODEL_ID");
  if (product_id && base::HexStringToUInt(product_id, &int_value)) {
    info->product_id = int_value;
    info->has_product_id = true;
  }

  const char* product_name_enc =
      udev_device_get_property_value(device.get(), "ID_MODEL_ENC");
  if (product_name_enc)
    info->display_name = device::UdevDecodeString(product_name_enc);

  const char* serial_number =
      udev_device_get_property_value(device.get(), "ID_SERIAL_SHORT");
  if (serial_number)
    info->serial_number = serial_number;

  SERIAL_LOG(EVENT) << "Serial device added: path=" << info->path
                    << " vid=" << (vendor_id ? vendor_id : "(none)")
                    << " pid=" << (product_id ? product_id : "(none)")
                    << " serial=" << info->serial_number.value_or("(none)");

  paths_.insert(std::make_pair(syspath, token));
  AddPort(std::move(info));
}

}  // namespace device
