Repository: ubnt-intrepid/dot Branch: main Commit: 3aba0a6ffa89 Files: 20 Total size: 33.9 KB Directory structure: gitextract_ylvdtk3u/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── ci/ │ ├── before_deploy.ps1 │ ├── before_deploy.sh │ ├── install.sh │ └── script.sh ├── rust-toolchain.toml ├── scripts/ │ └── bootstrap.sh ├── src/ │ ├── app.rs │ ├── dotfiles.rs │ ├── entry.rs │ ├── lib.rs │ ├── main.rs │ ├── util.rs │ └── windows.rs ├── templates/ │ └── mappings-example.toml └── tests/ └── dotfiles/ └── .mappings ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: - main tags: - 'v*' pull_request: branches: - main env: CARGO_TERM_VERBOSE: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run test run: cargo test ================================================ FILE: .gitignore ================================================ target/ *.tar.gz dot-*/ completions/ ================================================ FILE: Cargo.toml ================================================ [package] name = "dot" version = "0.2.0-dev" publish = false description = "Alternative of dotfile management frameworks" edition = "2018" [dependencies] ansi_term = "0.9" clap = "3" clap_complete = "3" error-chain = "0.12.1" regex = "1.11" shellexpand = "1" toml = "0.4" url = "2.5" dirs = "2.0.2" [target.'cfg(windows)'.dependencies] winapi = "0.2.8" advapi32-sys = "0.2.0" kernel32-sys = "0.2.2" runas = "0.1.1" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Yusuke Sasaki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # `dot` ![GitHub Actions](https://github.com/ubnt-intrepid/dot/workflows/Workflow/badge.svg) `dot` is a command-line tool for managing dotfiles, written in Rust. ## Overview `dot` provides a way to organize configuration files in your home directory. ## Installation Precompiled binaries are on our [GitHub releases page](https://github.com/ubnt-intrepid/dot/releases/latest). If you want to use the development version, try `cargo install` to build from source: ```shell-session $ cargo install --git https://github.com/ubnt-intrepid/dot.git ``` ## Example Usage Clone your dotfiles repository from github and then create home directory symlinks: ```sh $ dot init ubnt-intrepid/dotfiles ``` Check if all of the links exist and are correct: ```sh $ dot check ``` `` determines the remote repository's URL of dotfiles. Pattern types: * `(http|https|ssh|git)://[username@]github.com[:port]/path-to-repo.git` – URL of dotfiles repository * `git@github.com:path-to-repo.git` – SCP-like path * `username/dotfiles` – GitHub user and repository * `username` – GitHub user only (repository `dotfiles`, e.g.: `https://github.com/myuser/dotfiles`) By default, the repository will be cloned locally to `$HOME/.dotfiles`. This can be overridden with `$DOT_DIR`. For more information, run `dot help`. ## Configuration `$DOT_DIR/.mappings` where the symlinks are defined in [TOML](https://github.com/toml-lang/toml). For example: ```toml [general] gitconfig = "~/.gitconfig" "vim/vimrc" = ["~/.vimrc", "~/.config/nvim/init.vim"] #... [windows] vscode = "$APPDATA/Code/User" powershell = "$HOME/Documents/WindowsPowerShell" #... [linux] xinitrc = "~/.xinitrc" ``` Use `[general]` for symlinks on all platforms. `[windows]`, `[linux]`, `[macos]` for symlinks on specific platforms. See [my dotfiles](https://github.com/ubnt-intrepid/dotfiles) for a real example. ## License `dot` is distributed under the MIT license. See [LICENSE](LICENSE) for details. ## Similar Projects - [ssh0/dot](https://github.com/ssh0/dot) written in shell script - [rhysd/dotfiles](https://github.com/rhysd/dotfiles) written in Golang ================================================ FILE: ci/before_deploy.ps1 ================================================ # This script takes care of packaging the build artifacts that will go in the # release zipfile $SRC_DIR = $PWD.Path $STAGE = [System.Guid]::NewGuid().ToString() Set-Location $ENV:Temp New-Item -Type Directory -Name $STAGE Set-Location $STAGE $ZIP = "$SRC_DIR\$($Env:CRATE_NAME)-$($Env:APPVEYOR_REPO_TAG_NAME)-$($Env:TARGET).zip" # TODO Update this to package the right artifacts Copy-Item "$SRC_DIR\target\$($Env:TARGET)\release\dot.exe" '.\' 7z a "$ZIP" * Push-AppveyorArtifact "$ZIP" Remove-Item *.* -Force Set-Location .. Remove-Item $STAGE Set-Location $SRC_DIR ================================================ FILE: ci/before_deploy.sh ================================================ #!/bin/bash # This script takes care of building your crate and packaging it for release set -ex main() { local src=$(pwd) \ stage= case $TRAVIS_OS_NAME in linux) stage=$(mktemp -d) ;; osx) stage=$(mktemp -d -t tmp) ;; esac test -f Cargo.lock || cargo generate-lockfile # TODO Update this to build the artifacts that matter to you cross rustc --bin dot --target $TARGET --release -- -C lto # TODO Update this to package the right artifacts cp target/$TARGET/release/dot $stage/ cd $stage tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz * cd $src rm -rf $stage } main ================================================ FILE: ci/install.sh ================================================ #!/bin/bash # copied from trust set -ex main() { local target= if [ $TRAVIS_OS_NAME = linux ]; then target=x86_64-unknown-linux-musl sort=sort else target=x86_64-apple-darwin sort=gsort # for `sort --sort-version`, from brew's coreutils. fi # This fetches latest stable release local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \ | cut -d/ -f3 \ | grep -E '^v[0.1.0-9.]+$' \ | $sort --version-sort \ | tail -n1) curl -LSfs https://japaric.github.io/trust/install.sh | \ sh -s -- \ --force \ --git japaric/cross \ --tag $tag \ --target $target } main ================================================ FILE: ci/script.sh ================================================ #!/bin/bash set -ex # TODO This is the "test phase", tweak it as you see fit main() { cross build --target $TARGET --release if [ ! -z $DISABLE_TESTS ]; then return fi cross test --target $TARGET --release } # we don't run the "test phase" when doing deploys if [ -z $TRAVIS_TAG ]; then main fi ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "stable" profile = "minimal" components = [ "rustfmt", "clippy" ] ================================================ FILE: scripts/bootstrap.sh ================================================ #!/bin/bash -e # Usage: # DOTURL=https://github.com/ubnt-intrepid/.dotfiles.git [PREFIX=$HOME/.local] ./bootstrap.sh # Repository URL of your dotfiles. DOT_URL=${DOT_URL:-"https://github.com/ubnt-intrepid/.dotfiles.git"} # DOT_DIR=${DOT_DIR:-"$HOME/.dotfiles"} # installation directory of `dot` PREFIX=${PREFIX:-"$HOME/.local"} # --- export as environment variables export DOT_DIR # --- download `dot.rs` from GitHub Releases and install case `uname -s | tr '[A-Z]' '[a-z]'` in *mingw* | *msys*) DOTRS_SUFFIX="`uname -m`-windows-msvc" ;; *darwin*) DOTRS_SUFFIX="`uname -m`-apple-darwin" ;; *linux*) DOTRS_SUFFIX="`uname -m`-unknown-linux-musl" ;; *android*) # TODO: support for other architectures DOTRS_SUFFIX="arm-linux-androideabi" ;; *) echo "[fatal] cannot recognize the platform." exit 1 esac DOTRS_URL="`curl -s https://api.github.com/repos/ubnt-intrepid/dot.rs/releases | grep browser_download_url | cut -d '"' -f 4 | grep "$DOTRS_SUFFIX" | head -n 1`" echo "$DOTRS_URL" mkdir -p "${PREFIX}/bin" curl -sL "${DOTRS_URL}" | tar xz -C "$PREFIX/bin/" --strip=1 './dot' export PATH="$PREFIX/bin:$PATH" # --- clone your dotfiles into home directory, and make links. [[ -d "$DOT_DIR" ]] || git clone "$DOT_URL" "$DOT_DIR" dot link --verbose ================================================ FILE: src/app.rs ================================================ use crate::dotfiles::Dotfiles; use crate::errors::Result; use crate::util; use dirs; use regex::Regex; use std::borrow::Borrow; use std::env; use std::path::Path; use url::Url; #[cfg(windows)] use crate::windows; pub struct App { dotfiles: Dotfiles, dry_run: bool, verbose: bool, } impl App { pub fn new(dry_run: bool, verbose: bool) -> Result { let dotdir = init_envs()?; let dotfiles = Dotfiles::new(Path::new(&dotdir).to_path_buf()); Ok(App { dotfiles: dotfiles, dry_run: dry_run, verbose: verbose, }) } pub fn command_clone(&self, query: &str) -> Result { let url = resolve_url(query)?; let dotdir = self.dotfiles.root_dir().to_string_lossy(); util::wait_exec( "git", &["clone", url.as_str(), dotdir.borrow()], None, self.dry_run, ) .map_err(Into::into) } pub fn command_root(&self) -> Result { println!("{}", self.dotfiles.root_dir().display()); Ok(0) } pub fn command_check(&mut self) -> Result { self.dotfiles.read_entries(); let mut num_unhealth = 0; for entry in self.dotfiles.entries() { if entry.check(self.verbose).unwrap() == false { num_unhealth += 1; } } Ok(num_unhealth) } pub fn command_link(&mut self) -> Result { self.dotfiles.read_entries(); if !self.dry_run { check_symlink_privilege(); } for entry in self.dotfiles.entries() { entry.mklink(self.dry_run, self.verbose).unwrap(); } Ok(0) } pub fn command_clean(&mut self) -> Result { self.dotfiles.read_entries(); for entry in self.dotfiles.entries() { entry.unlink(self.dry_run, self.verbose).unwrap(); } Ok(0) } } #[cfg(windows)] fn check_symlink_privilege() { use windows::ElevationType; match windows::get_elevation_type().unwrap() { ElevationType::Default => { match windows::enable_privilege("SeCreateSymbolicLinkPrivilege") { Ok(_) => (), Err(err) => panic!("failed to enable SeCreateSymbolicLinkPrivilege: {}", err), } } ElevationType::Limited => { panic!("should be elevate as an Administrator."); } ElevationType::Full => (), } } #[cfg(not(windows))] #[inline] pub fn check_symlink_privilege() {} fn init_envs() -> Result { if env::var("HOME").is_err() { env::set_var("HOME", dirs::home_dir().unwrap()); } let dotdir = env::var("DOT_DIR") .or(util::expand_full("$HOME/.dotfiles")) .map_err(|_| "failed to determine dotdir".to_string())?; env::set_var("DOT_DIR", dotdir.as_str()); env::set_var("dotdir", dotdir.as_str()); Ok(dotdir) } fn resolve_url(s: &str) -> Result { let re_scheme = Regex::new(r"^([^:]+)://").unwrap(); let re_scplike = Regex::new(r"^((?:[^@]+@)?)([^:]+):/?(.+)$").unwrap(); if let Some(cap) = re_scheme.captures(s) { match cap.get(1).unwrap().as_str() { "http" | "https" | "ssh" | "git" | "file" => Url::parse(s).map_err(Into::into), scheme => Err(format!("'{}' is invalid scheme", scheme).into()), } } else if let Some(cap) = re_scplike.captures(s) { let username = cap .get(1) .and_then(|s| { if s.as_str() != "" { Some(s.as_str()) } else { None } }) .unwrap_or("git@"); let host = cap.get(2).unwrap().as_str(); let path = cap.get(3).unwrap().as_str(); Url::parse(&format!("ssh://{}{}/{}.git", username, host, path)).map_err(Into::into) } else { let username = s .splitn(2, "/") .next() .ok_or("'username' is unknown".to_owned())?; let reponame = s.splitn(2, "/").skip(1).next().unwrap_or("dotfiles"); Url::parse(&format!("https://github.com/{}/{}.git", username, reponame)).map_err(Into::into) } } ================================================ FILE: src/dotfiles.rs ================================================ use crate::entry::Entry; use crate::util; use std::path::{Path, PathBuf}; use toml; pub struct Dotfiles { _root_dir: PathBuf, _entries: Vec, } impl Dotfiles { pub fn new(root_dir: PathBuf) -> Dotfiles { Dotfiles { _root_dir: root_dir, _entries: Vec::new(), } } pub fn read_entries(&mut self) { self._entries = read_entries(self._root_dir.as_path()); } pub fn root_dir(&self) -> &Path { self._root_dir.as_path() } pub fn entries(&self) -> &[Entry] { self._entries.as_slice() } } fn read_entries(root_dir: &Path) -> Vec { let ref entries = util::read_toml(root_dir.join(".mappings")).unwrap(); let mut buf = Vec::new(); read_entries_from_key(&mut buf, entries, root_dir, "general"); read_entries_from_key(&mut buf, entries, root_dir, util::OS_NAME); buf } fn new_entry(root_dir: &Path, key: &str, val: &str) -> Entry { let src = util::expand_full(&format!("{}/{}", root_dir.display(), key)).unwrap(); let mut dst = util::expand_full(val).unwrap(); if Path::new(&dst).is_relative() { dst = util::expand_full(&format!("$HOME/{}", val)).unwrap(); } Entry::new(&src, &dst) } fn read_entries_from_key( buf: &mut Vec, entries: &toml::value::Table, root_dir: &Path, key: &str, ) { if let Some(entries_table) = entries.get(key).and_then(|value| value.as_table()) { for (ref key, ref val) in entries_table.iter() { if let Some(val) = val.as_str() { buf.push(new_entry(root_dir, key, val)); } if let Some(val) = val.as_array() { for v in val { if let Some(v) = v.as_str() { buf.push(new_entry(root_dir, key, v)); } } } } } } #[cfg(test)] mod tests { use super::{read_entries_from_key, Dotfiles}; use crate::util; use std::path::Path; #[test] fn smoke_test() { let root_dir = Path::new("tests/dotfiles").to_path_buf(); let mut dotfiles = Dotfiles::new(root_dir); assert_eq!(Path::new("tests/dotfiles"), dotfiles.root_dir()); dotfiles.read_entries(); } #[test] fn do_nothing_if_given_key_is_not_exist() { let root_dir = Path::new("tests/dotfiles").to_path_buf(); let entries = util::read_toml(root_dir.join(".mappings")).unwrap(); let mut buf = Vec::new(); read_entries_from_key(&mut buf, &entries, &root_dir, "hogehoge"); assert_eq!(buf.len(), 0); } } ================================================ FILE: src/entry.rs ================================================ use crate::util; use ansi_term; use std::fs; use std::io; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EntryStatus { Healthy, LinkNotCreated, NotSymLink, WrongLinkPath, } #[derive(Debug, Clone)] pub struct Entry { src: PathBuf, dst: PathBuf, } impl Entry { pub fn new(src: &str, dst: &str) -> Entry { Entry { src: util::make_pathbuf(src), dst: util::make_pathbuf(dst), } } pub fn status(&self) -> Result { let status = if !self.dst.exists() { EntryStatus::LinkNotCreated } else if !util::is_symlink(&self.dst)? { EntryStatus::NotSymLink } else if self.src != self.dst.read_link()? { EntryStatus::WrongLinkPath } else { EntryStatus::Healthy }; Ok(status) } pub fn check(&self, verbose: bool) -> Result { let status = self.status()?; if status != EntryStatus::Healthy { println!( "{} {} ({:?})", ansi_term::Style::new() .bold() .fg(ansi_term::Colour::Red) .paint("✘"), self.dst.display(), status ); return Ok(false); } if verbose { println!( "{} {}\n => {}", ansi_term::Style::new() .bold() .fg(ansi_term::Colour::Green) .paint("✓"), self.dst.display(), self.src.display() ); } Ok(true) } pub fn mklink(&self, dry_run: bool, verbose: bool) -> Result<(), io::Error> { if !self.src.exists() || self.status()? == EntryStatus::Healthy { return Ok(()); // Do nothing. } if self.dst.exists() && !util::is_symlink(&self.dst)? { let origpath = orig_path(&self.dst); println!( "file {} has already existed. It will be renamed to {}", self.dst.display(), origpath.display() ); fs::rename(&self.dst, origpath)?; } if verbose { println!("{}\n => {}", self.dst.display(), self.src.display()); } util::make_link(&self.src, &self.dst, dry_run) } pub fn unlink(&self, dry_run: bool, verbose: bool) -> Result<(), io::Error> { if !self.dst.exists() || !util::is_symlink(&self.dst)? { return Ok(()); // do nothing } if verbose { println!("unlink {}", self.dst.display()); } util::remove_link(&self.dst, dry_run)?; let origpath = orig_path(&self.dst); if origpath.exists() { fs::rename(origpath, &self.dst)?; } Ok(()) } } fn orig_path>(path: P) -> PathBuf { let origpath = format!("{}.bk", path.as_ref().to_str().unwrap()); Path::new(&origpath).to_path_buf() } ================================================ FILE: src/lib.rs ================================================ extern crate ansi_term; extern crate shellexpand; extern crate toml; #[macro_use] extern crate error_chain; extern crate regex; extern crate url; #[cfg(windows)] extern crate advapi32; #[cfg(windows)] extern crate kernel32; #[cfg(windows)] extern crate winapi; pub mod app; mod dotfiles; mod entry; pub mod util; #[cfg(windows)] mod windows; mod errors { error_chain! { foreign_links { Io(::std::io::Error); UrlParse(::url::ParseError); } } } pub use crate::errors::*; pub use crate::app::App; ================================================ FILE: src/main.rs ================================================ extern crate clap; extern crate dot; use clap::{AppSettings, Arg, SubCommand}; use dot::App; pub fn main() { match run() { Ok(retcode) => std::process::exit(retcode), Err(err) => panic!("unknown error: {}", err), } } pub fn run() -> dot::Result { let matches = cli().get_matches(); let dry_run = matches.is_present("dry-run"); let verbose = matches.is_present("verbose"); let mut app = App::new(dry_run, verbose)?; match matches.subcommand() { Some(("check", _)) => app.command_check(), Some(("link", _)) => app.command_link(), Some(("clean", _)) => app.command_clean(), Some(("root", _)) => app.command_root(), Some(("clone", args)) => { let url = args.value_of("url").unwrap(); app.command_clone(url) } Some(("init", args)) => { let url = args.value_of("url").unwrap(); let ret = app.command_clone(url)?; if ret != 0 { return Ok(ret); } app.command_link() } Some(("completion", args)) => { let shell: clap_complete::Shell = args.value_of_t_or_exit("shell"); clap_complete::generate( shell, &mut cli(), env!("CARGO_PKG_NAME"), &mut std::io::stdout(), ); Ok(0) } Some(..) | None => unreachable!(), } } fn cli() -> clap::Command<'static> { clap::Command::new(env!("CARGO_PKG_NAME")) .about(env!("CARGO_PKG_DESCRIPTION")) .version(env!("CARGO_PKG_VERSION")) .author(env!("CARGO_PKG_AUTHORS")) .setting(AppSettings::SubcommandRequiredElseHelp) .arg( Arg::with_name("verbose") .help("Use verbose output") .long("verbose") .short('v'), ) .arg( Arg::with_name("dry-run") .help("do not actually perform I/O operations") .long("dry-run") .short('n'), ) .subcommand( SubCommand::with_name("check") .about("Check the files are correctly linked to the right places"), ) .subcommand( SubCommand::with_name("link") .about("Create all of the symbolic links into home directory"), ) .subcommand( SubCommand::with_name("clean") .about("Remote all of registered links from home directory"), ) .subcommand( SubCommand::with_name("root") .about("Show the location of dotfiles repository and exit"), ) .subcommand( SubCommand::with_name("clone") .about("Clone dotfiles repository from remote") .arg( Arg::with_name("url") .help("URL of remote repository") .required(true) .takes_value(true), ), ) .subcommand( SubCommand::with_name("init") .about("Clone dotfiles repository from remote & make links") .arg( Arg::with_name("url") .help("URL of remote repository") .required(true) .takes_value(true), ), ) .subcommand( SubCommand::with_name("completion") .about("Generate completion scripts") .setting(AppSettings::ArgRequiredElseHelp) .arg( Arg::with_name("shell") .help("target shell") .required(true) .possible_values(&["bash", "fish", "zsh", "powershell"]), ), ) } ================================================ FILE: src/util.rs ================================================ use shellexpand::{self, LookupError}; use std::env; use std::fs::{self, File}; use std::io::{self, Read}; use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use std::process::{Command, Stdio}; use toml; #[allow(dead_code)] pub fn wait_exec( cmd: &str, args: &[&str], curr_dir: Option<&Path>, dry_run: bool, ) -> Result { if dry_run { println!("{} {:?} (@ {:?})", cmd, args, curr_dir); return Ok(0); } let mut command = Command::new(cmd); command .args(args) .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); if let Some(curr_dir) = curr_dir { command.current_dir(curr_dir); } let mut child = command.spawn()?; child .wait() .and_then(|st| st.code().ok_or(io::Error::new(io::ErrorKind::Other, ""))) } pub fn expand_full(s: &str) -> Result> { shellexpand::full(s).map(|s| s.into_owned()) } #[cfg(windows)] fn symlink, Q: AsRef>(src: P, dst: Q) -> Result<(), io::Error> { use std::os::windows::fs; if src.as_ref().is_dir() { fs::symlink_dir(src, dst) } else { fs::symlink_file(src, dst) } } #[cfg(not(windows))] fn symlink, Q: AsRef>(src: P, dst: Q) -> Result<(), io::Error> { use std::os::unix::fs::symlink; symlink(src, dst) } pub fn make_link(src: P, dst: Q, dry_run: bool) -> Result<(), io::Error> where P: AsRef, Q: AsRef, { if dry_run { println!( "make_link({}, {})", src.as_ref().display(), dst.as_ref().display() ); Ok(()) } else { fs::create_dir_all(dst.as_ref().parent().unwrap())?; symlink(src, dst) } } #[cfg(windows)] fn unlink>(dst: P) -> Result<(), io::Error> { if dst.as_ref().is_dir() { fs::remove_dir(dst) } else { fs::remove_file(dst) } } #[cfg(not(windows))] fn unlink>(dst: P) -> Result<(), io::Error> { fs::remove_file(dst) } pub fn remove_link>(dst: P, dry_run: bool) -> Result<(), io::Error> { if dry_run { println!("fs::remove_file {}", dst.as_ref().display()); Ok(()) } else { unlink(dst) } } pub fn read_toml>(path: P) -> Result { let mut file = File::open(path)?; let mut buf = Vec::new(); file.read_to_end(&mut buf)?; let content = String::from_utf8_lossy(&buf[..]).into_owned(); toml::de::from_str(&content).map_err(|_| { io::Error::new( io::ErrorKind::Other, "failed to parse configuration file as TOML", ) }) } #[cfg(target_os = "windows")] pub static OS_NAME: &'static str = "windows"; #[cfg(target_os = "macos")] pub static OS_NAME: &'static str = "darwin"; #[cfg(target_os = "linux")] pub static OS_NAME: &'static str = "linux"; #[cfg(target_os = "android")] pub static OS_NAME: &'static str = "linux"; #[cfg(target_os = "freebsd")] pub static OS_NAME: &'static str = "freebsd"; #[cfg(target_os = "openbsd")] pub static OS_NAME: &'static str = "openbsd"; // create an instance of PathBuf from string. pub fn make_pathbuf(path: &str) -> PathBuf { let path = path.replace("/", &format!("{}", MAIN_SEPARATOR)); Path::new(&path).to_path_buf() } pub fn is_symlink>(path: P) -> Result { let meta = path.as_ref().symlink_metadata()?; Ok(meta.file_type().is_symlink()) } ================================================ FILE: src/windows.rs ================================================ // #![allow(non_camel_case_types)] #![allow(non_snake_case)] #![allow(dead_code)] #![allow(improper_ctypes)] use advapi32; use kernel32; use std::ffi::CString; use std::mem; use std::os::raw::c_void; use std::ptr::{null, null_mut}; use winapi::winerror::ERROR_SUCCESS; use winapi::winnt; use winapi::LUID; type BYTE = u8; type BOOL = i32; type DWORD = u32; #[repr(C)] struct TOKEN_ELEVATION { TokenIsElevated: DWORD, } type TOKEN_ELEVATION_TYPE = u32; #[repr(C)] struct TOKEN_GROUPS { GroupCount: DWORD, Groups: [SID_AND_ATTRIBUTES; 0], } #[repr(C)] struct SID_AND_ATTRIBUTES { Sid: PSID, Attributes: DWORD, } #[repr(C)] struct SID_IDENTIFIER_AUTHORITY { Value: [BYTE; 6], } #[repr(C)] #[allow(improper_ctypes)] struct SID; type PSID = *mut SID; type PSID_IDENTIFIER_AUTHORITY = *mut SID_IDENTIFIER_AUTHORITY; #[allow(dead_code)] enum TOKEN_INFORMATION_CLASS { TokenUser = 1, TokenGroups, TokenPrivileges, TokenOwner, TokenPrimaryGroup, TokenDefaultDacl, TokenSource, TokenType, TokenImpersonationLevel, TokenStatistics, TokenRestrictedSids, TokenSessionId, TokenGroupsAndPrivileges, TokenSessionReference, TokenSandBoxInert, TokenAuditPolicy, TokenOrigin, TokenElevationType, TokenLinkedToken, TokenElevation, TokenHasRestrictions, TokenAccessInformation, TokenVirtualizationAllowed, TokenVirtualizationEnabled, TokenIntegrityLevel, TokenUIAccess, TokenMandatoryPolicy, TokenLogonSid, TokenIsAppContainer, TokenCapabilities, TokenAppContainerSid, TokenAppContainerNumber, TokenUserClaimAttributes, TokenDeviceClaimAttributes, TokenRestrictedUserClaimAttributes, TokenRestrictedDeviceClaimAttributes, TokenDeviceGroups, TokenRestrictedDeviceGroups, TokenSecurityAttributes, TokenIsRestricted, MaxTokenInfoClass, } extern "system" { fn GetTokenInformation( TokenHandle: winnt::HANDLE, TokenInformationClass: DWORD, TokenInformation: *mut c_void, TokenInformationLength: DWORD, ReturnLength: *mut DWORD, ) -> BOOL; fn IsUserAnAdmin() -> BOOL; fn AllocateAndInitializeSid( pIdentifierAuthority: PSID_IDENTIFIER_AUTHORITY, nSubAuthorityCount: BYTE, dwSubAuthority0: DWORD, dwSubAuthority1: DWORD, dwSubAuthority2: DWORD, dwSubAuthority3: DWORD, dwSubAuthority4: DWORD, dwSubAuthority5: DWORD, dwSubAuthority6: DWORD, dwSubAuthority7: DWORD, pSid: *mut PSID, ) -> BOOL; fn FreeSid(pSid: PSID) -> *mut c_void; fn CheckTokenMembership( TokenHandle: winnt::HANDLE, SidToCheck: PSID, IsMember: *mut BOOL, ) -> BOOL; } struct Handle(winnt::HANDLE); impl Handle { fn new(h: winnt::HANDLE) -> Handle { Handle(h) } fn as_raw(&self) -> winnt::HANDLE { self.0 } } impl Drop for Handle { fn drop(&mut self) { unsafe { kernel32::CloseHandle(self.0) }; self.0 = null_mut(); } } struct Sid(PSID); impl Sid { fn as_raw(&self) -> PSID { self.0 } } impl Drop for Sid { fn drop(&mut self) { unsafe { FreeSid(self.0) }; self.0 = null_mut(); } } pub fn enable_privilege(name: &str) -> Result<(), &'static str> { // 1. retrieve the process token of current process. let token = open_process_token(winnt::TOKEN_ADJUST_PRIVILEGES | winnt::TOKEN_QUERY)?; // 2. retrieve a LUID for given priviledge let luid = lookup_privilege_value(name)?; let len = mem::size_of::() + 1 * mem::size_of::(); let token_privileges = vec![0u8; len]; unsafe { let mut p = token_privileges.as_ptr() as *mut winnt::TOKEN_PRIVILEGES; let mut la = (*p).Privileges.as_ptr() as *mut winnt::LUID_AND_ATTRIBUTES; (*p).PrivilegeCount = 1; (*la).Luid = luid; (*la).Attributes = winnt::SE_PRIVILEGE_ENABLED; } unsafe { advapi32::AdjustTokenPrivileges( token.as_raw(), 0, token_privileges.as_ptr() as *mut winnt::TOKEN_PRIVILEGES, 0, null_mut(), null_mut(), ); } match unsafe { kernel32::GetLastError() } { ERROR_SUCCESS => Ok(()), _ => Err("failed to adjust token privilege"), } } pub fn is_elevated() -> Result { let token = open_process_token(winnt::TOKEN_QUERY)?; let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 }; let mut cb_size: u32 = mem::size_of_val(&elevation) as u32; let ret = unsafe { GetTokenInformation( token.as_raw(), mem::transmute::<_, u8>(TOKEN_INFORMATION_CLASS::TokenElevation) as u32, mem::transmute(&mut elevation), mem::size_of_val(&elevation) as u32, &mut cb_size, ) }; if ret == 0 { return Err("failed to get token information"); } Ok(elevation.TokenIsElevated != 0) } #[derive(Debug, PartialEq)] pub enum ElevationType { Default = 1, Full, Limited, } pub fn get_elevation_type() -> Result { let token = open_process_token(winnt::TOKEN_QUERY)?; let mut elev_type = 0; let mut cb_size = mem::size_of_val(&elev_type) as u32; let ret = unsafe { GetTokenInformation( token.as_raw(), mem::transmute::<_, u8>(TOKEN_INFORMATION_CLASS::TokenElevationType) as u32, mem::transmute(&mut elev_type), mem::size_of_val(&elev_type) as u32, &mut cb_size, ) }; if ret == 0 { return Err("failed to get token information"); } match elev_type { 1 => Ok(ElevationType::Default), // default (standard user/ administrator without UAC) 2 => Ok(ElevationType::Full), // full access (administrator, not elevated) 3 => Ok(ElevationType::Limited), // limited access (administrator, not elevated) _ => Err("unknown elevation type"), } } fn open_process_token(token_type: u32) -> Result { let mut h_token = null_mut(); let ret = unsafe { advapi32::OpenProcessToken(kernel32::GetCurrentProcess(), token_type, &mut h_token) }; match ret { 0 => Err("failed to get process token"), _ => Ok(Handle::new(h_token)), } } fn lookup_privilege_value(name: &str) -> Result { let mut luid = LUID { LowPart: 0, HighPart: 0, }; let ret = unsafe { let name = CString::new(name).unwrap(); advapi32::LookupPrivilegeValueA(null(), name.as_ptr(), &mut luid) }; match ret { 0 => Err("failed to get the privilege value"), _ => Ok(luid), } } ================================================ FILE: templates/mappings-example.toml ================================================ # vim: set ft=toml ts=2 sw=2 et : [general] "tmux.conf" = "~/.tmux.conf" gitconfig = "~/.gitconfig" tigrc = "~/.tigrc" zsh = "~/.config/zsh" "zsh/zshenv" = "~/.zshenv" "zsh/zshrc" = "~/.zshrc" vim = "~/.config/vim" "vim/vimrc" = "~/.vimrc" "vim/gvimrc" = "~/.gvimrc" [windows] atom = "~/.config/atom" mintty = "~/.config/mintty" "vscode/settings.json" = "$APPDATA/Code/User/settings.json" "vscode/locale.json" = "$APPDATA/Code/User/locale.json" powershell = "~/Documents/WindowsPowerShell" consolez = "$APPDATA/Console" "ConEmu.xml" = "$APPDATA/ConEmu.xml" [linux] neovim = "~/.config/nvim" xinitrc = "~/.xinitrc" termux = "~/.termux" "virtualenvwrapper/postactivate" = "~/.virtualenvs/postactivate" "virtualenvwrapper/postdeactivate" = "~/.virtualenvs/postdeactivate" ================================================ FILE: tests/dotfiles/.mappings ================================================ # vim: set ft=toml ts=2 sw=2 et : [general] "tmux.conf" = "~/.tmux.conf" gitconfig = ["~/.gitconfig", "~/.config/git/config"] tigrc = "~/.tigrc" zsh = "~/.config/zsh" "zsh/zshenv" = "~/.zshenv" "zsh/zshrc" = "~/.zshrc" vim = ["~/.vim", "~/.config/nvim"] "vim/vimrc" = "~/.vimrc" "vim/gvimrc" = "~/.gvimrc" [windows] atom = "~/.config/atom" mintty = "~/.config/mintty" "vscode/settings.json" = "$APPDATA/Code/User/settings.json" "vscode/locale.json" = "$APPDATA/Code/User/locale.json" powershell = "~/Documents/WindowsPowerShell" consolez = "$APPDATA/Console" "ConEmu.xml" = "$APPDATA/ConEmu.xml" [linux] neovim = "~/.config/nvim" xinitrc = "~/.xinitrc" termux = "~/.termux" "virtualenvwrapper/postactivate" = "~/.virtualenvs/postactivate" "virtualenvwrapper/postdeactivate" = "~/.virtualenvs/postdeactivate"