/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * 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
 *
 *      http://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 "src/profiling/common/proc_cmdline.h"

#include <fcntl.h>
#include <fnmatch.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "perfetto/ext/base/file_utils.h"

namespace perfetto {
namespace profiling {
namespace glob_aware {

// Edge cases: the raw cmdline as read out of the kernel can have several
// shapes, the process can rewrite the contents to be arbitrary, and overly long
// cmdlines can be truncated as we use a 511 byte limit. Some examples to
// consider for the implementation:
// * "echo\0hello\0"
// * "/bin/top\0\0\0\0\0\0\0"
// * "arbitrary string as rewritten by the process\0"
// * "some_bugged_kernels_forget_final_nul_terminator"
//
// The approach when performing the read->derive->match is to minimize early
// return codepaths for the caller. So even if we read a non-conforming cmdline
// (e.g. just a single nul byte), it can still be fed through FindBinaryName and
// MatchGlobPattern. It'll just make the intermediate strings be empty (so
// starting with a nul byte, but never nullptr).
//
// NB: bionic/libc/bionic/malloc_heapprofd will require a parallel
// implementation of these functions (to avoid a bionic->perfetto dependency).
// Keep them as STL-free as possible to allow for both implementations to be
// close to verbatim copies.

// TODO(rsavitski): consider changing to Optional<> return type.
bool ReadProcCmdlineForPID(pid_t pid, std::string* cmdline_out) {
  std::string filename = "/proc/" + std::to_string(pid) + "/cmdline";
  base::ScopedFile fd(base::OpenFile(filename, O_RDONLY));
  if (!fd) {
    PERFETTO_DPLOG("Failed to open %s", filename.c_str());
    return false;
  }

  // buf is 511 bytes to match an implementation that adds a null terminator to
  // the back of a 512 byte buffer.
  char buf[511];
  ssize_t rd = PERFETTO_EINTR(read(*fd, buf, sizeof(buf)));
  if (rd < 0) {
    PERFETTO_DPLOG("Failed to read %s", filename.c_str());
    return false;
  }

  cmdline_out->assign(buf, static_cast<size_t>(rd));
  return true;
}

// Returns a pointer into |cmdline| corresponding to the argv0 without any
// leading directories if the binary path is absolute. |cmdline_len| corresponds
// to the length of the cmdline string as read out of procfs as a C string -
// length doesn't include the final nul terminator, but it must be present at
// cmdline[cmdline_len]. Note that normally the string itself will contain nul
// bytes, as that's what the kernel uses to separate arguments.
//
// Function output examples:
// * /system/bin/adb\0--flag -> adb
// * adb -> adb
// * com.example.app -> com.example.app
const char* FindBinaryName(const char* cmdline, size_t cmdline_len) {
  // Find the first nul byte that signifies the end of argv0. We might not find
  // one if the process rewrote its cmdline without nul separators, and/or the
  // cmdline didn't fully fit into our read buffer. In such cases, proceed with
  // the full string to do best-effort matching.
  const char* argv0_end =
      static_cast<const char*>(memchr(cmdline, '\0', cmdline_len));
  if (argv0_end == nullptr) {
    argv0_end = cmdline + cmdline_len;  // set to final nul terminator
  }
  // Find the last path separator of argv0, if it exists.
  const char* name_start = static_cast<const char*>(
      memrchr(cmdline, '/', static_cast<size_t>(argv0_end - cmdline)));
  if (name_start == nullptr) {
    name_start = cmdline;
  } else {
    name_start++;  // skip the separator
  }
  return name_start;
}

// All inputs must be non-nullptr, but can start with a nul byte.
bool MatchGlobPattern(const char* pattern,
                      const char* cmdline,
                      const char* binname) {
  if (pattern[0] == '/') {
    return fnmatch(pattern, cmdline, FNM_NOESCAPE) == 0;
  }
  return fnmatch(pattern, binname, FNM_NOESCAPE) == 0;
}

}  // namespace glob_aware
}  // namespace profiling
}  // namespace perfetto
