ENIGMA Forums

Contributing to ENIGMA => Function Peer Review => Topic started by: time-killer-games on June 21, 2022, 12:57:11 am

Title: more code ripe for the picking
Post by: time-killer-games on June 21, 2022, 12:57:11 am
Code: [Select]
#if defined(_WIN32)
#include <Shlobj.h>
#elif defined(__APPLE__) && defined(__MACH__)
#include <sysdir.h>
#elif defined(__linux__)
#include <unistd.h>
#endif

namespace enigma_user {

  namespace {

    std::string directory_get_special_path(int dtype) {
      std::string result;
      #if defined(_WIN32)
      wchar_t *ptr = nullptr;
      KNOWNFOLDERID fid;
      switch (dtype) {
        case  0: { fid = FOLDERID_Desktop;   break; }
        case  1: { fid = FOLDERID_Documents; break; }
        case  2: { fid = FOLDERID_Downloads; break; }
        case  3: { fid = FOLDERID_Music;     break; }
        case  4: { fid = FOLDERID_Pictures;  break; }
        case  5: { fid = FOLDERID_Videos;    break; }
        default: { fid = FOLDERID_Desktop;   break; }
      }
      if (SUCCEEDED(SHGetKnownFolderPath(fid, KF_FLAG_CREATE | KF_FLAG_DONT_UNEXPAND, nullptr, &ptr))) {
        result = shorten(ptr);
        if (!result.empty() && result.back() != '\\') {
          result.push_back('\\');
        }
      }
      CoTaskMemFree(ptr);
      #elif defined(__APPLE__) && defined(__MACH__)
      char buf[PATH_MAX];
      sysdir_search_path_directory_t fid;
      sysdir_search_path_enumeration_state state;
      switch (dtype) {
        case  0: { fid = SYSDIR_DIRECTORY_DESKTOP;   break; }
        case  1: { fid = SYSDIR_DIRECTORY_DOCUMENT;  break; }
        case  2: { fid = SYSDIR_DIRECTORY_DOWNLOADS; break; }
        case  3: { fid = SYSDIR_DIRECTORY_MUSIC;     break; }
        case  4: { fid = SYSDIR_DIRECTORY_PICTURES;  break; }
        case  5: { fid = SYSDIR_DIRECTORY_MOVIES;    break; }
        default: { fid = SYSDIR_DIRECTORY_DESKTOP;   break; }
      }
      state = sysdir_start_search_path_enumeration(fid, SYSDIR_DOMAIN_MASK_USER);
      while ((state = sysdir_get_next_search_path_enumeration(state, buf))) {
        if (buf[0] == '~') {
          result = buf;
          result.replace(0, 1, environment_get_variable("HOME"));
          if (!result.empty() && result.back() != '/') {
            result.push_back('/');
          }
          break;
        }
      }
      #else
      std::string fid;
      switch (dtype) {
        case  0: { fid = "XDG_DESKTOP_DIR=";   break; }
        case  1: { fid = "XDG_DOCUMENTS_DIR="; break; }
        case  2: { fid = "XDG_DOWNLOAD_DIR=";  break; }
        case  3: { fid = "XDG_MUSIC_DIR=";     break; }
        case  4: { fid = "XDG_PICTURES_DIR=";  break; }
        case  5: { fid = "XDG_VIDEOS_DIR=";    break; }
        default: { fid = "XDG_DESKTOP_DIR=";   break; }
      }
      std::string conf = environment_get_variable("HOME") + "/.config/user-dirs.dirs";
      if (file_exists(conf)) {
        int dirs = file_text_open_read(conf);
        if (dirs != -1) {
          while (!file_text_eof(dirs)) {
            std::string line = file_text_read_string(dirs);
            file_text_readln(dirs);
            std::size_t pos = line.find(fid, 0);
            if (pos != std::string::npos) {
              FILE *fp = popen(("echo " + line.substr(pos + fid.length())).c_str(), "r");
              if (fp) {
                char buf[PATH_MAX];
                if (fgets(buf, PATH_MAX, fp)) {
                  std::string str = buf;
                  std::size_t pos = str.find("\n", strlen(buf) - 1);
                  if (pos != std::string::npos) {
                    str.replace(pos, 1, "");
                  }
                  if (!directory_exists(str)) {
                    directory_create(str);
                  }
                  result = str;
                  if (!result.empty() && result.back() != '/') {
                    result.push_back('/');
                  }
                }
                pclose(fp);
              }
            }
          }
          file_text_close(dirs);
        }
      }
      #endif
      return result;
    }

  } // anonymous namespace

  std::string directory_get_desktop_path() {
    return directory_get_special_path(0);
  }

  string directory_get_documents_path() {
    return directory_get_special_path(1);
  }

  std::string directory_get_downloads_path() {
    return directory_get_special_path(2);
  }

  std::string directory_get_music_path() {
    return directory_get_special_path(3);
  }

  std::string directory_get_pictures_path() {
    return directory_get_special_path(4);
  }

  std::string directory_get_videos_path() {
    return directory_get_special_path(5);
  }

} // namespace enigma_user

I am using popen()/fgets()/pclose() for expanding environment variables which is a requirement for correctly parsing the user-dirs.dirs config file:

Code: [Select]
freebsd@freebsd:~ $ cat ~/.config/user-dirs.dirs
# This file is written by xdg-user-dirs-update
# If you want to change or add directories, just edit the line you're
# interested in. All local changes will be retained on the next run.
# Format is XDG_xxx_DIR="$HOME/yyy", where yyy is a shell-escaped
# homedir-relative path, or XDG_xxx_DIR="/yyy", where /yyy is an
# absolute path. No other format is supported.
#
XDG_DESKTOP_DIR="$HOME/Desktop"
XDG_DOWNLOAD_DIR="$HOME/Downloads"
XDG_TEMPLATES_DIR="$HOME/Templates"
XDG_PUBLICSHARE_DIR="$HOME/Public"
XDG_DOCUMENTS_DIR="$HOME/Documents"
XDG_MUSIC_DIR="$HOME/Music"
XDG_PICTURES_DIR="$HOME/Pictures"
XDG_VIDEOS_DIR="$HOME/Videos"

It's important to note these paths will not always be English due to the user-chosen system locale. This goes for all supported platforms. The path may have more than one environment variable as well as escaped quotes within the initial quoted paths on Linux and similar platforms. I don't provide a function for Templates because that is not available on macOS and on Windows it isn't in the home folder (%USERPROFILE% is basically the Windows-equivalent of $HOME on the other platforms) like it is on Linux. I don't have a function for Public because it isn't always in the home folder depending on the platform, thus aren't platform equivalents.

An important note from the Windows implementation which uses SHGetKnownFolderPath():

Quote
When this method returns, contains the address of a pointer to a null-terminated Unicode string that specifies the path of the known folder. The calling process is responsible for freeing this resource once it is no longer needed by calling CoTaskMemFree, whether SHGetKnownFolderPath succeeds or not.

...which is why i am calling CoTaskMemFree() regardless of whether the call to SHGetKnownFolderPath() succeeds or not.

Documentation citation: https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath

The paths returned on all platforms will contain a trailing slash at the end of the string. The paths will be created if they do not already exist on Windows or Linux. Mac is guaranteed to have the paths exist already, as they can't be deleted.