├── cli ├── LICENSE.txt ├── src │ ├── cmd │ │ ├── self │ │ │ ├── uninstall.rs │ │ │ ├── update.rs │ │ │ ├── rev.rs │ │ │ └── mod.rs │ │ ├── search.rs │ │ ├── list.rs │ │ ├── uninstall.rs │ │ ├── submit │ │ │ ├── mod.rs │ │ │ ├── bug.rs │ │ │ └── feature.rs │ │ ├── home.rs │ │ ├── docs.rs │ │ ├── source.rs │ │ ├── update.rs │ │ ├── new.rs │ │ ├── package.rs │ │ ├── login.rs │ │ ├── prelude.rs │ │ ├── mod.rs │ │ ├── ship.rs │ │ ├── config │ │ │ └── mod.rs │ │ ├── run.rs │ │ └── install.rs │ ├── main.rs │ ├── macros.rs │ └── cli │ │ ├── exec.rs │ │ └── mod.rs ├── Cargo.toml ├── build.rs └── README.md ├── lib ├── LICENSE.txt ├── src │ ├── auth │ │ ├── mod.rs │ │ └── credentials.rs │ ├── drop │ │ ├── kind │ │ │ ├── lib.rs │ │ │ ├── app.rs │ │ │ ├── font.rs │ │ │ ├── mod.rs │ │ │ └── exe.rs │ │ ├── license │ │ │ ├── serde.rs │ │ │ └── mod.rs │ │ ├── manifest │ │ │ ├── deps.rs │ │ │ ├── meta.rs │ │ │ ├── mod.rs │ │ │ └── tests.rs │ │ ├── source │ │ │ ├── mod.rs │ │ │ └── git.rs │ │ ├── version.rs │ │ ├── mod.rs │ │ ├── package.rs │ │ └── name │ │ │ ├── mod.rs │ │ │ ├── parse.rs │ │ │ └── scoped.rs │ ├── env.rs │ ├── api │ │ ├── v1 │ │ │ ├── mod.rs │ │ │ ├── download.rs │ │ │ ├── ship.rs │ │ │ └── login.rs │ │ └── mod.rs │ ├── archive.rs │ ├── path.rs │ ├── lib.rs │ ├── install │ │ ├── mod.rs │ │ └── target.rs │ ├── config │ │ ├── mod.rs │ │ ├── user.rs │ │ ├── rt.rs │ │ └── file.rs │ ├── system.rs │ └── flexible.rs ├── Cargo.toml └── README.md ├── shared ├── LICENSE.txt ├── src │ ├── ext │ │ ├── mod.rs │ │ ├── path.rs │ │ ├── ffi.rs │ │ ├── process.rs │ │ └── bytes.rs │ └── lib.rs ├── Cargo.toml └── README.md ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── CI.yml ├── Cargo.toml ├── examples └── meaning-of-life │ ├── Ocean.toml │ └── meaning-of-life ├── rustfmt.toml ├── README.md ├── .gitignore ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /cli/LICENSE.txt: -------------------------------------------------------------------------------- 1 | ../LICENSE.txt -------------------------------------------------------------------------------- /lib/LICENSE.txt: -------------------------------------------------------------------------------- 1 | ../LICENSE.txt -------------------------------------------------------------------------------- /shared/LICENSE.txt: -------------------------------------------------------------------------------- 1 | ../LICENSE.txt -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ['nvzqz'] 2 | patreon: oceanpkg 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cli", "lib", "shared"] 3 | -------------------------------------------------------------------------------- /lib/src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | //! Authentication utilities. 2 | 3 | pub mod credentials; 4 | 5 | #[doc(inline)] 6 | pub use self::credentials::Credentials; 7 | -------------------------------------------------------------------------------- /shared/src/ext/mod.rs: -------------------------------------------------------------------------------- 1 | //! Extension traits. 2 | 3 | mod bytes; 4 | mod ffi; 5 | mod path; 6 | mod process; 7 | 8 | pub use self::{bytes::*, ffi::*, path::*, process::*}; 9 | -------------------------------------------------------------------------------- /examples/meaning-of-life/Ocean.toml: -------------------------------------------------------------------------------- 1 | [meta] 2 | name = "meaning-of-life" 3 | display-name = "Meaning of Life" 4 | description = "Gives the answer to the question of the meaning of life, the universe, and everything." 5 | version = "0.1.0" 6 | -------------------------------------------------------------------------------- /cli/src/cmd/self/uninstall.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub const NAME: &str = "uninstall"; 4 | 5 | pub fn cmd() -> App { 6 | SubCommand::with_name(NAME).about("Uninstall Ocean") 7 | } 8 | 9 | pub fn run(_state: &mut Config, _matches: &ArgMatches) -> crate::Result { 10 | unimplemented!() 11 | } 12 | -------------------------------------------------------------------------------- /cli/src/cmd/self/update.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub const NAME: &str = "update"; 4 | 5 | pub fn cmd() -> App { 6 | SubCommand::with_name(NAME).about("Download and install updates to Ocean") 7 | } 8 | 9 | pub fn run(_state: &mut Config, _matches: &ArgMatches) -> crate::Result { 10 | unimplemented!() 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/drop/kind/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::drop::Metadata; 2 | 3 | /// A package for a library of a given language. 4 | #[derive(Clone, Debug)] 5 | pub struct Lib { 6 | metadata: Metadata, 7 | } 8 | 9 | impl Lib { 10 | /// Returns basic metadata for the drop. 11 | #[inline] 12 | pub const fn metadata(&self) -> &Metadata { 13 | &self.metadata 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/meaning-of-life/meaning-of-life: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This file should never have the execution bit set, to demonstrate Ocean being 4 | # able to install and run it without. 5 | 6 | echo "The answer to the question of the meaning of life, the universe, and everything is..." 7 | sleep 1 8 | 9 | if [[ -z $1 ]]; then 10 | echo "42" 11 | else 12 | echo "$1" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/src/drop/kind/app.rs: -------------------------------------------------------------------------------- 1 | use crate::drop::Metadata; 2 | 3 | /// A package with a graphical interface. 4 | #[derive(Clone, Debug)] 5 | pub struct App { 6 | metadata: Metadata, 7 | file_name: String, 8 | } 9 | 10 | impl App { 11 | /// Returns basic metadata for the drop. 12 | #[inline] 13 | pub const fn metadata(&self) -> &Metadata { 14 | &self.metadata 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/env.rs: -------------------------------------------------------------------------------- 1 | //! Environment variables used by Ocean. 2 | 3 | /// The parent `ocean` executable. 4 | pub const OCEAN: &str = "OCEAN"; 5 | 6 | /// Ocean's `bin` directory. 7 | pub const OCEAN_BIN_DIR: &str = "OCEAN_BIN_DIR"; 8 | 9 | /// Ocean's current version. 10 | pub const OCEAN_VERSION: &str = "OCEAN_VERSION"; 11 | 12 | /// Ocean's API URL. 13 | pub const OCEAN_API_URL: &str = "OCEAN_API_URL"; 14 | -------------------------------------------------------------------------------- /lib/src/drop/kind/font.rs: -------------------------------------------------------------------------------- 1 | use crate::drop::Metadata; 2 | 3 | /// A package for a typeface with specific properties; e.g. bold, italic. 4 | #[derive(Clone, Debug)] 5 | pub struct Font { 6 | metadata: Metadata, 7 | file_name: String, 8 | } 9 | 10 | impl Font { 11 | /// Returns basic metadata for the drop. 12 | #[inline] 13 | pub const fn metadata(&self) -> &Metadata { 14 | &self.metadata 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Unix" 5 | use_small_heuristics = "Default" 6 | reorder_imports = true 7 | reorder_modules = true 8 | remove_nested_parens = true 9 | fn_args_layout = "Tall" 10 | edition = "2018" 11 | merge_derives = true 12 | use_try_shorthand = false 13 | use_field_init_shorthand = true 14 | force_explicit_abi = true 15 | print_misformatted_file_names = true 16 | -------------------------------------------------------------------------------- /lib/src/api/v1/mod.rs: -------------------------------------------------------------------------------- 1 | //! Version 1 of Ocean's web API. 2 | 3 | /// The default URL to which API calls are made: `https://api.oceanpkg.org/v1/`. 4 | pub const DEFAULT_URL: &str = "https://api.oceanpkg.org/v1/"; 5 | 6 | #[cfg(feature = "reqwest")] 7 | mod download; 8 | 9 | #[cfg(feature = "reqwest")] 10 | mod login; 11 | 12 | #[cfg(feature = "reqwest")] 13 | mod ship; 14 | 15 | #[cfg(feature = "reqwest")] 16 | #[doc(inline)] 17 | pub use self::{download::*, login::*, ship::*}; 18 | -------------------------------------------------------------------------------- /shared/src/ext/path.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | /// Extended functionality for 4 | /// [`PathBuf`](https://doc.rust-lang.org/std/path/struct.PathBuf.html). 5 | pub trait PathBufExt { 6 | /// Like `join`, only reusing the underlying buffer. 7 | fn pushing>(self, path: P) -> PathBuf; 8 | } 9 | 10 | impl PathBufExt for PathBuf { 11 | fn pushing>(mut self, path: P) -> PathBuf { 12 | self.push(path); 13 | self 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cli/src/cmd/search.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use clap::ArgMatches; 3 | 4 | pub const NAME: &str = "search"; 5 | 6 | pub fn cmd() -> App { 7 | SubCommand::with_name(NAME).about("Search for drop(s)").arg( 8 | Arg::with_name("drop") 9 | .help("The package name(s) to search for") 10 | .multiple(true) 11 | .required(true), 12 | ) 13 | } 14 | 15 | pub fn run(_state: &mut Config, _matches: &ArgMatches) -> crate::Result { 16 | unimplemented!() 17 | } 18 | -------------------------------------------------------------------------------- /cli/src/cmd/self/rev.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | 3 | pub const NAME: &str = "rev"; 4 | 5 | pub fn cmd() -> App { 6 | SubCommand::with_name(NAME) 7 | .about("Prints the git revision used to build Ocean") 8 | } 9 | 10 | pub fn run(_state: &mut Config, _matches: &ArgMatches) -> crate::Result { 11 | if let Some(rev) = crate::GIT_REV { 12 | println!("{}", rev); 13 | Ok(()) 14 | } else { 15 | Err(failure::err_msg( 16 | "`ocean` was not built with `git` available", 17 | )) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/drop/kind/mod.rs: -------------------------------------------------------------------------------- 1 | //! Drop kinds. 2 | 3 | mod app; 4 | mod exe; 5 | mod font; 6 | mod lib; 7 | 8 | #[doc(inline)] 9 | pub use self::{app::App, exe::Exe, font::Font, lib::Lib}; 10 | 11 | /// The type of package a drop can be. 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 13 | pub enum Kind { 14 | /// Has a graphical interface. 15 | App, 16 | /// Can be executed; e.g. CLI tool or script. 17 | Exe, 18 | /// A typeface with specific properties; e.g. bold, italic. 19 | Font, 20 | /// A library of a given language. 21 | Lib, 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/archive.rs: -------------------------------------------------------------------------------- 1 | //! Archiving utilities. 2 | 3 | use flate2::read::GzDecoder; 4 | use std::{io, path::Path}; 5 | 6 | /// Reads `tarball` as a `.tar.gz` file and unpacks it to `path`. 7 | /// 8 | /// Because `GzDecoder` uses a buffered reader internally, this is appropriate 9 | /// to call on `File`s. 10 | pub fn unpack_tarball(tarball: R, path: P) -> io::Result<()> 11 | where 12 | R: io::Read, 13 | P: AsRef, 14 | { 15 | let decoder = GzDecoder::new(tarball); 16 | println!("{:#?}", decoder.header()); 17 | tar::Archive::new(decoder).unpack(path) 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/auth/credentials.rs: -------------------------------------------------------------------------------- 1 | //! User credentials. 2 | 3 | /// User credentials. 4 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 5 | pub struct Credentials { 6 | /// Credentials for the main Ocean registry. 7 | pub registry: Option>, 8 | } 9 | 10 | /// Credentials for the main Ocean registry. 11 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 12 | pub struct Registry { 13 | /// A token associated with a specific user that provides permissions for 14 | /// interacting with packages in a registry. 15 | pub token: S, 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Shared reusable library for the [Ocean package manager][home]. 2 | //! 3 | //! This library is meant to be used internally by Ocean's crates, and not meant 4 | //! for use outside of this ecosystem. 5 | //! 6 | //! [home]: https://www.oceanpkg.org 7 | //! [CLI]: https://en.wikipedia.org/wiki/Command-line_interface 8 | //! [GUI]: https://en.wikipedia.org/wiki/Graphical_user_interface 9 | 10 | #![deny(missing_docs)] 11 | #![doc(html_root_url = "https://docs.rs/oceanpkg-shared/0.1.0")] 12 | #![doc(html_logo_url = "https://www.oceanpkg.org/static/images/ocean-logo.svg")] 13 | 14 | pub mod ext; 15 | -------------------------------------------------------------------------------- /shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oceanpkg-shared" 3 | description = "Shared reusable library for the Ocean package manager." 4 | version = "0.1.2" 5 | authors = ["The Ocean Project Developers"] 6 | license = "AGPL-3.0-only" 7 | readme = "README.md" 8 | edition = "2018" 9 | homepage = "https://www.oceanpkg.org" 10 | repository = "https://github.com/oceanpkg/ocean" 11 | documentation = "https://docs.rs/oceanpkg-shared" 12 | include = ["Cargo.toml", "src/**/*.rs", "README.md", "CHANGELOG.md", "LICENSE*"] 13 | 14 | [target.'cfg(windows)'.dependencies] 15 | winapi = { version = "0.3", features = ["consoleapi", "minwindef"] } 16 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate oceanpkg; 3 | 4 | #[macro_use] 5 | mod macros; 6 | 7 | mod cli; 8 | mod cmd; 9 | 10 | type Result = failure::Fallible; 11 | 12 | /// The git revision for the version built. 13 | pub const GIT_REV: Option<&str> = option_env!("OCEAN_GIT_REV"); 14 | 15 | const ABOUT: &str = " 16 | Flexibly manages packages 17 | 18 | See https://www.oceanpkg.org for more info."; 19 | 20 | fn main() { 21 | let mut config = oceanpkg::Config::create() 22 | .unwrap_or_else(|error| exit_error!("error: {}", error)); 23 | 24 | if let Err(error) = cli::main(&mut config) { 25 | exit_error!(error); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cli/src/cmd/list.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | pub const NAME: &str = "list"; 4 | 5 | pub fn cmd() -> App { 6 | SubCommand::with_name(NAME) 7 | .about("List installed drops") 8 | .arg( 9 | Arg::user_flag() 10 | .help("List drops locally available to a specific user"), 11 | ) 12 | .arg( 13 | Arg::global_flag() 14 | .help("List drops globally available to all users"), 15 | ) 16 | } 17 | 18 | pub fn run(_state: &mut Config, matches: &ArgMatches) -> crate::Result { 19 | let install_target = matches.install_target(); 20 | unimplemented!("TODO: List for {:?}", install_target); 21 | } 22 | -------------------------------------------------------------------------------- /shared/src/ext/ffi.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | 3 | /// Extended functionality for 4 | /// [`OsStr`](https://doc.rust-lang.org/std/ffi/struct.OsStr.html). 5 | pub trait OsStrExt { 6 | /// Attempts to retrieve the underlying bytes of `self`. 7 | fn try_as_bytes(&self) -> Option<&[u8]>; 8 | } 9 | 10 | impl OsStrExt for OsStr { 11 | #[inline] 12 | fn try_as_bytes(&self) -> Option<&[u8]> { 13 | #[cfg(unix)] 14 | { 15 | use std::os::unix::ffi::OsStrExt; 16 | Some(self.as_bytes()) 17 | } 18 | 19 | // To get the bytes on non-Unix platforms, `OsStr` needs to be converted 20 | // to a `str` first. 21 | #[cfg(not(unix))] 22 | { 23 | self.to_str().map(|s| s.as_bytes()) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oceanpkg-cli" 3 | description = "Command-line interface for the Ocean package manager." 4 | version = "0.0.0" 5 | authors = ["The Ocean Project Developers"] 6 | license = "AGPL-3.0-only" 7 | readme = "README.md" 8 | edition = "2018" 9 | homepage = "https://www.oceanpkg.org" 10 | publish = false 11 | build = "build.rs" 12 | 13 | [[bin]] 14 | name = "ocean" 15 | path = "src/main.rs" 16 | 17 | [build-dependencies] 18 | cargo-emit = "0.1" 19 | 20 | [dependencies] 21 | oceanpkg = { path = "../lib", version = "0.0.11", features = ["reqwest", "toml"] } 22 | oceanpkg-shared = { path = "../shared", version = "0.1.2" } 23 | clap = "2.33" 24 | dirs = "2" 25 | failure = "0.1.6" 26 | percent-encoding = "2" 27 | reqwest = "0.9.22" 28 | rpassword = "4" 29 | toml = "0.5" 30 | url = "2.1" 31 | -------------------------------------------------------------------------------- /cli/src/cmd/uninstall.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | pub const NAME: &str = "uninstall"; 4 | 5 | pub fn cmd() -> App { 6 | SubCommand::with_name(NAME) 7 | .about("Removes a drop") 8 | .arg(Arg::global_flag().help("Remove the drop for all users")) 9 | .arg( 10 | Arg::with_name("drop") 11 | .help("The package(s) to remove") 12 | .multiple(true) 13 | .required(true), 14 | ) 15 | } 16 | 17 | pub fn run(_state: &mut Config, matches: &ArgMatches) -> crate::Result { 18 | let install_target = matches.install_target(); 19 | println!("Uninstalling for {:?}", install_target); 20 | 21 | if let Some(values) = matches.values_of_os("drop") { 22 | for value in values { 23 | println!("Uninstalling {:?}...", value); 24 | } 25 | } 26 | 27 | unimplemented!() 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/path.rs: -------------------------------------------------------------------------------- 1 | use shared::ext::*; 2 | use std::{ 3 | env, 4 | ffi::OsStr, 5 | io, iter, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | pub fn resolve_exe( 10 | exe: &Path, 11 | path_var: Option<&OsStr>, 12 | ) -> io::Result { 13 | if let Some(path_var) = path_var { 14 | let candidates = env::split_paths(path_var).flat_map(|path| { 15 | let candidate = path.pushing(exe); 16 | let with_exe = if env::consts::EXE_EXTENSION.is_empty() { 17 | None 18 | } else { 19 | Some(candidate.with_extension(env::consts::EXE_EXTENSION)) 20 | }; 21 | iter::once(candidate).chain(with_exe) 22 | }); 23 | for candidate in candidates { 24 | if candidate.is_file() { 25 | return candidate.canonicalize(); 26 | } 27 | } 28 | } 29 | exe.canonicalize() 30 | } 31 | -------------------------------------------------------------------------------- /cli/src/cmd/submit/mod.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::system::open; 3 | 4 | mod bug; 5 | mod feature; 6 | 7 | pub const NAME: &str = "submit"; 8 | 9 | pub fn cmd() -> App { 10 | SubCommand::with_name(NAME) 11 | .about("Creates a pre-populated GitHub issue") 12 | .arg( 13 | Arg::with_name("kind") 14 | .help("The type of issue: 'bug' or 'feature'") 15 | .possible_values(&["bug", "feature"]) 16 | .hide_possible_values(true) // Format this ourselves. 17 | .required(true), 18 | ) 19 | } 20 | 21 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 22 | let url = match matches.value_of("kind") { 23 | Some("bug") => bug::url(config), 24 | Some("feature") => feature::url(config), 25 | _ => unreachable!(), // Required argument 26 | }; 27 | open(&[url])?; 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /cli/src/cmd/self/mod.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | mod rev; 4 | mod uninstall; 5 | mod update; 6 | 7 | pub const NAME: &str = "self"; 8 | 9 | pub fn cmd() -> App { 10 | SubCommand::with_name(NAME) 11 | .about("Modify the Ocean installation") 12 | .settings(&[ 13 | AppSettings::SubcommandRequiredElseHelp, 14 | AppSettings::DeriveDisplayOrder, 15 | ]) 16 | .subcommands(vec![rev::cmd(), update::cmd(), uninstall::cmd()]) 17 | } 18 | 19 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 20 | if let (command, Some(matches)) = matches.subcommand() { 21 | let run = match command { 22 | rev::NAME => rev::run, 23 | update::NAME => update::run, 24 | uninstall::NAME => uninstall::run, 25 | _ => unreachable!("could not match command {:?}", command), 26 | }; 27 | run(config, matches) 28 | } else { 29 | // SubcommandRequiredElseHelp 30 | unreachable!() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oceanpkg" 3 | description = "Client library for the Ocean package manager." 4 | version = "0.0.11" 5 | authors = ["The Ocean Project Developers"] 6 | license = "AGPL-3.0-only" 7 | readme = "README.md" 8 | edition = "2018" 9 | homepage = "https://www.oceanpkg.org" 10 | repository = "https://github.com/oceanpkg/ocean" 11 | documentation = "https://docs.rs/oceanpkg" 12 | include = ["Cargo.toml", "src/**/*.rs", "README.md", "CHANGELOG.md", "LICENSE*"] 13 | 14 | [dependencies] 15 | oceanpkg-shared = { version = "0.1.2", path = "../shared" } 16 | cfg-if = "0.1" 17 | dirs = "1" 18 | flate2 = "1" 19 | http = "0.1" 20 | lazy_static = "1.4" 21 | lazycell = "1.2" 22 | linfo = { version = "0.1.3", features = ["phf", "serde"] } 23 | reqwest = { version = "0.9.22", optional = true } 24 | semver = { version = "0.9", features = ["serde"] } 25 | serde = { version = "1", features = ["derive"] } 26 | serde_json = "1" 27 | static_assertions = "1.1" 28 | tar = "0.4" 29 | toml = { version = "0.5", optional = true } 30 | url = "2.1" 31 | 32 | [dev-dependencies] 33 | tempfile = "3" 34 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Client library for the [Ocean package manager][home]. 2 | //! 3 | //! This library is meant to be used by Ocean's [CLI], [GUI], and web services. 4 | //! 5 | //! [home]: https://www.oceanpkg.org 6 | //! [CLI]: https://en.wikipedia.org/wiki/Command-line_interface 7 | //! [GUI]: https://en.wikipedia.org/wiki/Graphical_user_interface 8 | 9 | #![deny(missing_docs)] 10 | #![doc(html_root_url = "https://docs.rs/oceanpkg/0.0.11")] 11 | #![doc(html_logo_url = "https://www.oceanpkg.org/static/images/ocean-logo.svg")] 12 | 13 | extern crate oceanpkg_shared as shared; 14 | 15 | #[macro_use] 16 | extern crate cfg_if; 17 | #[macro_use] 18 | extern crate lazy_static; 19 | #[macro_use] 20 | extern crate serde; 21 | #[macro_use] 22 | extern crate static_assertions; 23 | 24 | extern crate serde_json as json; 25 | 26 | #[macro_use] 27 | pub(crate) mod flexible; 28 | 29 | pub mod api; 30 | pub mod archive; 31 | pub mod auth; 32 | pub mod config; 33 | pub mod drop; 34 | pub mod env; 35 | pub mod install; 36 | pub mod system; 37 | 38 | mod path; 39 | 40 | #[doc(inline)] 41 | pub use self::{config::Config, drop::Drop}; 42 | -------------------------------------------------------------------------------- /cli/src/cmd/home.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::system::open; 3 | 4 | pub const NAME: &str = "home"; 5 | 6 | const OCEAN_HOME: &str = "https://www.oceanpkg.org/"; 7 | 8 | pub fn cmd() -> App { 9 | SubCommand::with_name(NAME) 10 | .about("Opens the homepage in a browser") 11 | .arg( 12 | Arg::with_name("drops") 13 | .help("The drops to open home pages for") 14 | .multiple(true), 15 | ) 16 | .arg( 17 | Arg::with_name("print") 18 | .short("p") 19 | .long("print") 20 | .help("Simply print the homepage URL"), 21 | ) 22 | } 23 | 24 | pub fn run(_state: &mut Config, matches: &ArgMatches) -> crate::Result { 25 | let print_home = matches.is_present("print"); 26 | 27 | if let Some(drops) = matches.values_of("drops") { 28 | for drop in drops { 29 | unimplemented!("TODO: Open the homepage for {:?}", drop); 30 | } 31 | } else if print_home { 32 | println!("{}", OCEAN_HOME); 33 | } else { 34 | open(&[OCEAN_HOME])?; 35 | } 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/install/mod.rs: -------------------------------------------------------------------------------- 1 | //! Describes how drops are installed. 2 | //! 3 | //! Drops can either be installed specifically for the current user executing 4 | //! Ocean or globally, making them available to _all_ users. 5 | 6 | use std::{error::Error, fmt}; 7 | 8 | mod target; 9 | 10 | #[doc(inline)] 11 | pub use self::target::InstallTarget; 12 | 13 | /// A directory for an `InstallTarget` could not be retrieved. 14 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 15 | pub enum DirError { 16 | /// Could not get the current user's home directory. 17 | CurrentUserHome, 18 | /// Could not get the current user's configuration directory. 19 | CurrentUserConfigDir, 20 | } 21 | 22 | impl Error for DirError {} 23 | 24 | impl fmt::Display for DirError { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | match self { 27 | Self::CurrentUserHome => { 28 | write!(f, "Could not get current user's home directory") 29 | } 30 | Self::CurrentUserConfigDir => { 31 | write!(f, "Could not get current user's config directory") 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cli/src/cmd/docs.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::system::open; 3 | 4 | pub const NAME: &str = "docs"; 5 | 6 | const OCEAN_DOCS: &str = "https://docs.oceanpkg.org/"; 7 | 8 | pub fn cmd() -> App { 9 | SubCommand::with_name(NAME) 10 | .about("Opens documentation in a browser") 11 | .arg( 12 | Arg::with_name("drops") 13 | .help("The drops to open documentation for") 14 | .multiple(true), 15 | ) 16 | .arg( 17 | Arg::with_name("print") 18 | .short("p") 19 | .long("print") 20 | .help("Simply print the documentation URL"), 21 | ) 22 | } 23 | 24 | pub fn run(_state: &mut Config, matches: &ArgMatches) -> crate::Result { 25 | let print_docs = matches.is_present("print"); 26 | 27 | if let Some(drops) = matches.values_of("drops") { 28 | for drop in drops { 29 | unimplemented!("TODO: Open the documentation page for {:?}", drop); 30 | } 31 | } else if print_docs { 32 | println!("{}", OCEAN_DOCS); 33 | } else { 34 | open(&[OCEAN_DOCS])?; 35 | } 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /cli/src/cmd/source.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::system::open; 3 | 4 | pub const NAME: &str = "source"; 5 | 6 | const OCEAN_REPO: &str = "https://github.com/oceanpkg/ocean/"; 7 | 8 | pub fn cmd() -> App { 9 | SubCommand::with_name(NAME) 10 | .about("Opens the source code repository in a browser") 11 | .arg( 12 | Arg::with_name("drops") 13 | .help("The drops to the repository for") 14 | .multiple(true), 15 | ) 16 | .arg( 17 | Arg::with_name("print") 18 | .short("p") 19 | .long("print") 20 | .help("Simply print the repository URL"), 21 | ) 22 | } 23 | 24 | pub fn run(_state: &mut Config, matches: &ArgMatches) -> crate::Result { 25 | let print_repo = matches.is_present("print"); 26 | 27 | if let Some(drops) = matches.values_of("drops") { 28 | for drop in drops { 29 | unimplemented!("TODO: Open the repository page for {:?}", drop); 30 | } 31 | } else if print_repo { 32 | println!("{}", OCEAN_REPO); 33 | } else { 34 | open(&[OCEAN_REPO])?; 35 | } 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /cli/src/cmd/update.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | pub const NAME: &str = "update"; 4 | 5 | pub fn cmd() -> App { 6 | SubCommand::with_name(NAME) 7 | .about("Updates installed packages") 8 | .arg( 9 | Arg::all_flag() 10 | .help("Update all drops") 11 | .conflicts_with("drop"), 12 | ) 13 | .arg(Arg::global_flag().help("Update the drop for all users")) 14 | .arg(Arg::user_flag().help("Update the drop for a specific user")) 15 | .arg( 16 | Arg::with_name("drop") 17 | .help("The package(s) to update") 18 | .multiple(true) 19 | .required_unless("all"), 20 | ) 21 | } 22 | 23 | pub fn run(_state: &mut Config, matches: &ArgMatches) -> crate::Result { 24 | let install_target = matches.install_target(); 25 | println!("Updating for {:?}", install_target); 26 | 27 | if let Some(values) = matches.values_of_os("drop") { 28 | for value in values { 29 | println!("Updating {:?}...", value); 30 | } 31 | } else if matches.is_present("all") { 32 | println!("Updating all packages...") 33 | } 34 | 35 | unimplemented!() 36 | } 37 | -------------------------------------------------------------------------------- /cli/src/cmd/new.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::drop::Name; 3 | use std::path::Path; 4 | 5 | pub const NAME: &str = "new"; 6 | 7 | pub fn cmd() -> App { 8 | SubCommand::with_name(NAME) 9 | .about("Create a new, pre-filled drop manifest") 10 | .arg( 11 | Arg::with_name("name").takes_value(true).help( 12 | "The name of the drop; default is current directory name", 13 | ), 14 | ) 15 | .arg( 16 | Arg::with_name("path") 17 | .takes_value(true) 18 | .long("path") 19 | .short("p") 20 | .help("The path where to write the manifest"), 21 | ) 22 | } 23 | 24 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 25 | let path: &Path = match matches.value_of_os("path") { 26 | Some(path) => path.as_ref(), 27 | None => &config.rt.current_dir, 28 | }; 29 | 30 | let name = path.file_name().unwrap_or("".as_ref()); 31 | match Name::new(name) { 32 | Ok(name) => { 33 | unimplemented!("TODO: Spit out manifest for '{}'", name); 34 | } 35 | Err(error) => unimplemented!("TODO: Handle '{}'", error), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! End-user configuration of the local Ocean installation. 2 | //! 3 | //! This is tied heavily to `InstallTarget`. 4 | 5 | pub mod file; 6 | pub mod rt; 7 | pub mod user; 8 | 9 | #[doc(inline)] 10 | pub use self::{ 11 | file::{ConfigFile, ConfigFileFmt}, 12 | rt::RtConfig, 13 | user::UserConfig, 14 | }; 15 | 16 | /// Configuration values that are examined throughout the lifetime of a client 17 | /// program. 18 | /// 19 | /// This type supports interior mutability and thus is not [`Sync`]. Note that 20 | /// despite this, it is still [`Send`]. 21 | /// 22 | /// [`Send`]: https://doc.rust-lang.org/std/marker/trait.Send.html 23 | /// [`Sync`]: https://doc.rust-lang.org/std/marker/trait.Sync.html 24 | #[derive(Clone, Debug)] 25 | pub struct Config { 26 | /// User configuration values. 27 | pub user: UserConfig, 28 | /// Runtime configuration values. 29 | pub rt: RtConfig, 30 | } 31 | 32 | assert_impl_all!(Config: Send); 33 | assert_not_impl_all!(Config: Sync); 34 | 35 | impl Config { 36 | /// Creates a new instance suitable for using at the start of your program. 37 | #[inline] 38 | pub fn create() -> Result { 39 | let rt = RtConfig::create()?; 40 | let user = UserConfig::new(); 41 | Ok(Self { user, rt }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cli/src/cmd/package.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | pub const NAME: &str = "package"; 4 | 5 | pub fn cmd() -> App { 6 | SubCommand::with_name(NAME) 7 | .about("Assemble the local package into a distributable tarball") 8 | .arg( 9 | Arg::with_name("manifest") 10 | .help("Path to Ocean.toml") 11 | .long("manifest") 12 | .takes_value(true), 13 | ) 14 | .arg( 15 | Arg::with_name("output") 16 | .help("The directory where to output") 17 | .short("o") 18 | .long("output") 19 | .takes_value(true), 20 | ) 21 | } 22 | 23 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 24 | let package = oceanpkg::drop::Package::create( 25 | config.rt.current_dir(), 26 | matches.value_of_os("manifest"), 27 | matches.value_of_os("output"), 28 | )?; 29 | // Get duration immediately after packaging finishes. 30 | let elapsed = config.rt.time_elapsed(); 31 | 32 | let tarball = &package.path; 33 | let tarball = match tarball.strip_prefix(config.rt.current_dir()) { 34 | Ok(suffix) => suffix, 35 | Err(_) => &tarball, 36 | }; 37 | 38 | println!("Successfully packaged \"{}\"!", tarball.display()); 39 | println!("Finished in {:?}", elapsed); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, process}; 2 | 3 | fn main() { 4 | emit_target_info(); 5 | emit_git_rev(); 6 | } 7 | 8 | /// Make the target's system info available to the build. 9 | fn emit_target_info() { 10 | let mut target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); 11 | let target_os = match target_os.as_str() { 12 | "macos" => "macOS", 13 | _ => { 14 | target_os[..1].make_ascii_lowercase(); 15 | &target_os 16 | } 17 | }; 18 | cargo_emit::rustc_env!("OCEAN_TARGET_OS", "{}", target_os); 19 | 20 | let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 21 | cargo_emit::rustc_env!("OCEAN_TARGET_ARCH", "{}", target_arch); 22 | } 23 | 24 | /// Make the current git revision hash available to the build. 25 | fn emit_git_rev() { 26 | let git_output = process::Command::new("git") 27 | .args(&["rev-parse", "HEAD"]) 28 | .output(); 29 | match git_output { 30 | Ok(git_output) => match String::from_utf8(git_output.stdout) { 31 | Ok(rev) => { 32 | let rev = rev.trim(); 33 | if !rev.is_empty() { 34 | cargo_emit::rustc_env!("OCEAN_GIT_REV", "{}", rev); 35 | } 36 | } 37 | Err(error) => { 38 | cargo_emit::warning!("Could not parse git hash: {}", error); 39 | } 40 | }, 41 | Err(error) => cargo_emit::warning!("Could not run `git`: {}", error), 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/drop/license/serde.rs: -------------------------------------------------------------------------------- 1 | use super::AnyLicense; 2 | use serde::{ 3 | de::{self, Deserialize, Deserializer, Visitor}, 4 | ser::{Serialize, Serializer}, 5 | }; 6 | use std::fmt; 7 | 8 | struct LicenseVisitor; 9 | 10 | impl<'de> Visitor<'de> for LicenseVisitor { 11 | type Value = AnyLicense<'de>; 12 | 13 | #[inline] 14 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 15 | f.write_str("a license string") 16 | } 17 | 18 | #[inline] 19 | fn visit_str(self, v: &str) -> Result 20 | where 21 | E: de::Error, 22 | { 23 | Ok(AnyLicense::owned(v)) 24 | } 25 | 26 | #[inline] 27 | fn visit_borrowed_str(self, v: &'de str) -> Result 28 | where 29 | E: de::Error, 30 | { 31 | Ok(v.into()) 32 | } 33 | 34 | #[inline] 35 | fn visit_string(self, v: String) -> Result 36 | where 37 | E: de::Error, 38 | { 39 | Ok(AnyLicense::owned(v)) 40 | } 41 | } 42 | 43 | impl<'de: 'a, 'a> Deserialize<'de> for AnyLicense<'a> { 44 | #[inline] 45 | fn deserialize(deserializer: D) -> Result 46 | where 47 | D: Deserializer<'de>, 48 | { 49 | deserializer.deserialize_str(LicenseVisitor) 50 | } 51 | } 52 | 53 | impl Serialize for AnyLicense<'_> { 54 | #[inline] 55 | fn serialize(&self, s: S) -> Result { 56 | s.serialize_str(self.id()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cli/src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Prints a formatted message to [`stderr`] indicating that an error has 2 | /// occurred. 3 | // TODO: Switch to `log::error!`. 4 | macro_rules! error { 5 | ($fmt:literal $($args:tt)*) => { { 6 | eprintln!(concat!("error: ", $fmt) $($args)*); 7 | } }; 8 | } 9 | 10 | /// Prints a message to [`stderr`] and exits the process with an exit code of 1. 11 | /// 12 | /// If an identifier is passed, it will be printed using [`fmt::Display`]. 13 | /// 14 | /// [`stderr`]: https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr) 15 | /// [`fmt::Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html 16 | macro_rules! exit_error { 17 | ($fmt:literal $($args:tt)*) => { { 18 | eprintln!($fmt $($args)*); 19 | std::process::exit(1); 20 | } }; 21 | ($error:expr) => { 22 | exit_error!("{}", $error) 23 | }; 24 | } 25 | 26 | /// Analogous to what [`println!`] is for [`print!`], but for [`format!`]. 27 | /// 28 | /// [`println!`]: https://doc.rust-lang.org/std/macro.println.html 29 | /// [`print!`]: https://doc.rust-lang.org/std/macro.print.html 30 | /// [`format!`]: https://doc.rust-lang.org/std/macro.format.html 31 | macro_rules! formatln { 32 | ($fmt:literal $($args:tt)*) => { 33 | format!(concat!($fmt, newline!()) $($args)*) 34 | }; 35 | } 36 | 37 | #[cfg(unix)] 38 | macro_rules! newline { 39 | () => { 40 | "\n" 41 | }; 42 | } 43 | 44 | #[cfg(windows)] 45 | macro_rules! newline { 46 | () => { 47 | "\r\n" 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /cli/src/cmd/login.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::{ 3 | api, 4 | auth::credentials::{Credentials, Registry}, 5 | }; 6 | use std::fs; 7 | 8 | pub const NAME: &str = "login"; 9 | 10 | pub fn cmd() -> App { 11 | SubCommand::with_name(NAME) 12 | .about("Log into Ocean for shipping drops") 13 | .arg( 14 | Arg::with_name("username") 15 | .takes_value(true) 16 | .required(true) 17 | .help("The username with which to login"), 18 | ) 19 | } 20 | 21 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 22 | let username = matches 23 | .value_of("username") 24 | .unwrap_or_else(|| unreachable!("Required argument")); 25 | 26 | let password = rpassword::prompt_password_stdout(&formatln!( 27 | "Enter password for \"{}\":", 28 | username 29 | ))?; 30 | 31 | let credentials = api::v1::Credentials::basic_auth(username, &password); 32 | let token = api::v1::request_login_token(&credentials)?; 33 | 34 | // Serialize token into TOML string. 35 | let credentials = Credentials { 36 | registry: Some(Registry { token }), 37 | }; 38 | let toml = toml::to_string_pretty(&credentials)?; 39 | 40 | // Write credentials. 41 | let credentials_path = config.rt.credentials_path(); 42 | if let Some(parent) = credentials_path.parent() { 43 | fs::DirBuilder::new().recursive(true).create(parent)?; 44 | } 45 | fs::write(&credentials_path, &toml)?; 46 | 47 | println!("You are now logged in!"); 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /cli/src/cmd/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use clap::{AppSettings, ArgMatches, SubCommand}; 2 | pub use oceanpkg::{install::InstallTarget, Config}; 3 | 4 | pub type App = clap::App<'static, 'static>; 5 | pub type Arg = clap::Arg<'static, 'static>; 6 | 7 | /// Extended functionality for `ArgMatches`. 8 | pub trait ArgMatchesExt { 9 | /// Returns how drops get installed by checking whether a `"global"` 10 | /// argument was used. 11 | fn install_target(&self) -> InstallTarget; 12 | } 13 | 14 | impl ArgMatchesExt for ArgMatches<'_> { 15 | fn install_target(&self) -> InstallTarget { 16 | if self.is_present("global") { 17 | InstallTarget::Global 18 | } else if let Some(user) = self.value_of("user") { 19 | InstallTarget::SpecificUser(user.to_owned()) 20 | } else { 21 | InstallTarget::CurrentUser 22 | } 23 | } 24 | } 25 | 26 | pub trait ArgExt { 27 | /// The common `--all`/`-a` flag. 28 | fn all_flag() -> Self; 29 | 30 | /// The common `--global`/`-g` flag. 31 | fn global_flag() -> Self; 32 | 33 | /// The common `--user`/`-u` flag that takes a username value. 34 | fn user_flag() -> Self; 35 | } 36 | 37 | impl ArgExt for clap::Arg<'_, '_> { 38 | fn all_flag() -> Self { 39 | Arg::with_name("all").short("a").long("all") 40 | } 41 | 42 | fn global_flag() -> Self { 43 | Arg::with_name("global").short("g").long("global") 44 | } 45 | 46 | fn user_flag() -> Self { 47 | Arg::with_name("user") 48 | .short("u") 49 | .long("user") 50 | .takes_value(true) 51 | .number_of_values(1) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /shared/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Ocean logo 6 | 7 |
8 |

Ocean Shared Library

9 | 10 | Build Status 12 | 13 | 14 | Crates.io badge 16 | 17 |
18 |
19 | 20 | The `oceanpkg-shared` library serves as reusable components for: 21 | - the `oceanpkg` library 22 | - The `ocean` [CLI] client 23 | - Backend web services 24 | 25 | ## Usage 26 | 27 | This library is primarily meant for Ocean's components and not for external use. 28 | Because of this, you won't see parts of this library be publicly re-exported 29 | through the `oceanpkg` library. 30 | 31 | See [documentation]. 32 | 33 | ## Testing 34 | 35 | Various test cases are covered throughout this library. They can all be found by 36 | searching for `mod tests` within the `lib` folder. 37 | 38 | To perform these tests, simply run: 39 | 40 | ```sh 41 | cargo test 42 | ``` 43 | 44 | [CLI]: https://en.wikipedia.org/wiki/Command-line_interface 45 | [Rust]: https://www.rust-lang.org 46 | [`cargo`]: https://doc.rust-lang.org/cargo 47 | [rustup.rs]: https://rustup.rs 48 | [crate]: https://crates.io/crates/oceanpkg-shared 49 | [documentation]: https://docs.rs/oceanpkg-shared 50 | [`Cargo.toml`]: https://doc.rust-lang.org/cargo/reference/manifest.html 51 | -------------------------------------------------------------------------------- /lib/src/drop/manifest/deps.rs: -------------------------------------------------------------------------------- 1 | use crate::drop::{name::Query, source::Git}; 2 | use std::collections::BTreeMap; 3 | 4 | /// A mapping from drop names to dependency specification information. 5 | pub type Deps = BTreeMap; 6 | 7 | flexible! { 8 | /// The value associated with an element listed in the `dependencies` key in the 9 | /// manifest. 10 | /// 11 | /// This is defined as an `enum` to allow for flexibility in parsing. Either a 12 | /// simple string will be parsed, in which case it's a version number 13 | /// (`Version`), or a list of key/value pairs will be parsed (`Detailed`). 14 | /// 15 | /// In the future, this should be defined as a `struct` to ease usage in Rust 16 | /// while retaining flexibility in parsing. 17 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)] 18 | pub struct DepInfo { 19 | /// The version requirement string, e.g. `^1.0.0`. 20 | pub version: String, 21 | 22 | /// Whether the dependency is optional. The default is `false`. 23 | #[serde(default)] 24 | pub optional: bool, 25 | 26 | // Tables: all types that serialize into maps (or "tables" in TOML) 27 | // them must be placed last to succeed. 28 | 29 | /// What git repository can it be fetched from if requested via git as 30 | /// an alternative source. Note that this may differ from the 31 | /// dependency's own `git` field in its drop manifest. 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | pub git: Option, 34 | } 35 | } 36 | 37 | impl From for DepInfo { 38 | fn from(version: String) -> Self { 39 | Self { 40 | version, 41 | git: None, 42 | optional: false, 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: I have a suggestion (and may want to implement it)! 🙂 4 | title: "[Describe the feature you're proposing]" 5 | labels: kind/feature 6 | --- 7 | 8 | # 💡 Feature Request 9 | 10 | 13 | 14 | I have a dream that Ocean will be able to ... 15 | 16 | ## Components 17 | 18 | 25 | 26 | This would work on Ocean's ... 27 | 28 | ## Motivation 29 | 30 | 35 | 36 | We should do this because ... 37 | 38 | ## Proposal 39 | 40 | 44 | 45 | This can be achieved by ... 46 | 47 | ## Prior Art 48 | 49 | 56 | 57 | If you take a look at ..., you'll see that it has ... 58 | 59 | ## Alternatives 60 | 61 | 66 | 67 | We could also take the approach explained at ... 68 | -------------------------------------------------------------------------------- /lib/src/install/target.rs: -------------------------------------------------------------------------------- 1 | use super::DirError; 2 | use shared::ext::PathBufExt; 3 | use std::path::PathBuf; 4 | 5 | /// Indicates where to (un)install a drop. 6 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 7 | pub enum InstallTarget { 8 | /// Installation available for the current user; the default. 9 | /// 10 | /// In most places, it is usually assumed that this is what's wanted. 11 | CurrentUser, 12 | /// Installation available for a user, specified by a name. 13 | /// 14 | /// On the command-line, this is specified by the `--user`/`-u` flag. 15 | SpecificUser(String), 16 | /// Globally-available installation. 17 | /// 18 | /// On the command-line, this is specified by the `--global`/`-g` flag. 19 | Global, 20 | } 21 | 22 | impl Default for InstallTarget { 23 | #[inline] 24 | fn default() -> Self { 25 | InstallTarget::CurrentUser 26 | } 27 | } 28 | 29 | impl InstallTarget { 30 | /// Returns the configuration files directory for the installation target. 31 | /// 32 | /// # Examples 33 | /// 34 | /// For `InstallTarget::CurrentUser`, some expected outputs are: 35 | /// 36 | /// - Windows: `C:\Users\Alice\AppData\Roaming\Ocean` 37 | /// - macOS: `/Users/Alice/Library/Preferences/Ocean` 38 | /// - Linux: `/home/alice/.config` 39 | pub fn cfg_dir(&self) -> Result { 40 | match self { 41 | Self::CurrentUser => dirs::config_dir() 42 | .ok_or(DirError::CurrentUserConfigDir) 43 | .map(|cfg| cfg.pushing("Ocean")), 44 | Self::SpecificUser(username) => { 45 | unimplemented!("TODO: Get config directory for {:?}", username); 46 | } 47 | Self::Global => { 48 | unimplemented!("TODO: Get global config directory"); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! Interfacing with Ocean's web API. 2 | 3 | use std::{ 4 | borrow::Cow, 5 | env::{self, VarError}, 6 | ffi::{OsStr, OsString}, 7 | }; 8 | use url::Url; 9 | 10 | pub mod v1; 11 | 12 | /// The default URL to which API calls are made: `https://api.oceanpkg.org/`. 13 | pub const DEFAULT_URL: &str = "https://api.oceanpkg.org/"; 14 | 15 | /// The environment variable key for using an alternative API URL: 16 | /// `OCEAN_API_URL`. 17 | pub const URL_ENV_KEY: &str = crate::env::OCEAN_API_URL; 18 | 19 | /// Returns the parsed `Url` value for [`URL_ENV_VAR`] or [`DEFAULT_URL`] if it 20 | /// does not exist. 21 | /// 22 | /// [`URL_ENV_VAR`]: constant.URL_ENV_VAR.html 23 | /// [`DEFAULT_URL`]: constant.DEFAULT_URL.html 24 | pub fn url() -> Result { 25 | match url_str() { 26 | Ok(url) => Url::parse(&url), 27 | Err(_) => Err(url::ParseError::InvalidDomainCharacter), 28 | } 29 | } 30 | 31 | /// Returns the UTF-8 encoded value for [`URL_ENV_VAR`] or [`DEFAULT_URL`] if it 32 | /// does not exist. 33 | /// 34 | /// [`URL_ENV_VAR`]: constant.URL_ENV_VAR.html 35 | /// [`DEFAULT_URL`]: constant.DEFAULT_URL.html 36 | pub fn url_str() -> Result, OsString> { 37 | match env::var(URL_ENV_KEY) { 38 | Ok(var) => Ok(Cow::Owned(var)), 39 | Err(VarError::NotPresent) => Ok(Cow::Borrowed(DEFAULT_URL)), 40 | Err(VarError::NotUnicode(var)) => Err(var), 41 | } 42 | } 43 | 44 | /// Returns the OS encoded value for [`URL_ENV_VAR`] or [`DEFAULT_URL`] if it 45 | /// does not exist. 46 | /// 47 | /// [`URL_ENV_VAR`]: constant.URL_ENV_VAR.html 48 | /// [`DEFAULT_URL`]: constant.DEFAULT_URL.html 49 | pub fn url_os_str() -> Cow<'static, OsStr> { 50 | if let Some(var) = env::var_os(URL_ENV_KEY) { 51 | Cow::Owned(var) 52 | } else { 53 | Cow::Borrowed(DEFAULT_URL.as_ref()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 | Ocean logo 10 | 11 |
12 |

Ocean

13 | 14 | Build Status 16 | 17 | Lines of code 18 |
19 |
20 | 21 | The package manager from the future, coming to an operating system near you! 22 | 23 | ## Compatibility 24 | 25 | | Platform | Status | 26 | | :------- | :----- | 27 | | macOS | Actively developed | 28 | | Linux | Actively developed | 29 | | Windows | Future support planned | 30 | 31 | ## Command-Line Interface 32 | 33 | The `ocean` [CLI] client is the main way of using Ocean. 34 | 35 | See [`cli/README.md`] for info. 36 | 37 | ## Library 38 | 39 | [![Crates.io badge](https://img.shields.io/crates/v/oceanpkg.svg)](https://crates.io/crates/oceanpkg) 40 | 41 | The `oceanpkg` library serves as core reusable components for: 42 | - The `ocean` [CLI] client 43 | - Backend web services 44 | 45 | See [`lib/README.md`] or [docs.rs/oceanpkg] for info. 46 | 47 | ## License 48 | 49 | Ocean is licensed under the GNU Affero General Public License v3. 50 | See [`LICENSE.txt`] for full text. 51 | 52 | [CLI]: https://en.wikipedia.org/wiki/Command-line_interface 53 | [`LICENSE.txt`]: https://github.com/oceanpkg/ocean/blob/master/LICENSE.txt 54 | [`cli/README.md`]: https://github.com/oceanpkg/ocean/blob/master/cli/README.md 55 | [`lib/README.md`]: https://github.com/oceanpkg/ocean/blob/master/lib/README.md 56 | [docs.rs/oceanpkg]: https://docs.rs/oceanpkg 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,linux,windows,rust 3 | # Edit at https://www.gitignore.io/?templates=macos,linux,windows,rust 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### Rust ### 49 | # Generated by Cargo 50 | # will have compiled files and executables 51 | /target/ 52 | 53 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 54 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 55 | Cargo.lock 56 | 57 | # These are backup files generated by rustfmt 58 | **/*.rs.bk 59 | 60 | ### Windows ### 61 | # Windows thumbnail cache files 62 | Thumbs.db 63 | Thumbs.db:encryptable 64 | ehthumbs.db 65 | ehthumbs_vista.db 66 | 67 | # Dump file 68 | *.stackdump 69 | 70 | # Folder config file 71 | [Dd]esktop.ini 72 | 73 | # Recycle Bin used on file shares 74 | $RECYCLE.BIN/ 75 | 76 | # Windows Installer files 77 | *.cab 78 | *.msi 79 | *.msix 80 | *.msm 81 | *.msp 82 | 83 | # Windows shortcuts 84 | *.lnk 85 | 86 | # End of https://www.gitignore.io/api/macos,linux,windows,rust 87 | -------------------------------------------------------------------------------- /lib/src/drop/kind/exe.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | drop::{name::Query, Metadata}, 3 | install::InstallTarget, 4 | }; 5 | use std::{path::PathBuf, process::Command}; 6 | 7 | /// A package that can be executed; e.g. CLI tool or script. 8 | #[derive(Clone, Debug)] 9 | pub struct Exe { 10 | metadata: Metadata, 11 | bin_name: String, 12 | } 13 | 14 | impl Exe { 15 | /// Returns an executable matching `query`, installed for `target`. 16 | pub fn installed>( 17 | query: &Query, 18 | target: &InstallTarget, 19 | ) -> Result { 20 | let query = query.to_ref::(); 21 | unimplemented!( 22 | "TODO: Find installation of {:?} for {:?}", 23 | query, 24 | target 25 | ) 26 | } 27 | 28 | /// Returns basic metadata for the drop. 29 | #[inline] 30 | pub const fn metadata(&self) -> &Metadata { 31 | &self.metadata 32 | } 33 | 34 | /// Returns the name of the binary executable. 35 | #[inline] 36 | pub fn bin_name(&self) -> &str { 37 | &self.bin_name 38 | } 39 | 40 | /// Returns the path of the drop's executable binary, if one exists. 41 | pub fn bin_path<'t>( 42 | &self, 43 | target: &'t InstallTarget, 44 | ) -> Result> { 45 | unimplemented!( 46 | "TODO: Find {:?} binary for {:?}", 47 | self.metadata.name, 48 | target, 49 | ) 50 | } 51 | 52 | /// Returns a `Command` instance suitable for running the drop's executable 53 | /// binary, if one exists. 54 | pub fn command<'t>( 55 | &self, 56 | target: &'t InstallTarget, 57 | ) -> Result> { 58 | self.bin_path(target).map(Command::new) 59 | } 60 | } 61 | 62 | /// An error returned when the binary for an executable drop cannot be found. 63 | #[derive(Debug)] 64 | pub struct FindError<'a> { 65 | target: &'a InstallTarget, 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/drop/source/mod.rs: -------------------------------------------------------------------------------- 1 | //! The source of a package. 2 | 3 | use url::Url; 4 | 5 | pub mod git; 6 | 7 | pub use self::git::Git; 8 | 9 | use self::git::Ref; 10 | 11 | const OCEAN_REGISTRY: &str = "https://registry.oceanpkg.org"; 12 | 13 | lazy_static! { 14 | static ref OCEAN_REGISTRY_SOURCE: Source = 15 | Source::from_registry(Url::parse(OCEAN_REGISTRY).unwrap()); 16 | } 17 | 18 | /// The source of a drop. 19 | #[derive(Clone, Debug, PartialEq, Eq)] 20 | pub struct Source { 21 | url: Url, 22 | kind: Kind, 23 | } 24 | 25 | impl Source { 26 | /// A drop source for the main Ocean registry. 27 | #[inline] 28 | pub fn main_registry() -> &'static Self { 29 | &OCEAN_REGISTRY_SOURCE 30 | } 31 | 32 | /// A drop source at a `Url` for an Ocean registry. 33 | #[inline] 34 | pub const fn from_registry(url: Url) -> Self { 35 | Source { 36 | url, 37 | kind: Kind::Registry, 38 | } 39 | } 40 | 41 | /// A drop source at a `Url` for to a git repository. 42 | #[inline] 43 | pub fn from_git(url: Url) -> Self { 44 | Self::from_git_at(url, Ref::master()) 45 | } 46 | 47 | /// A drop source at a `Url` for a git repository at a specific reference. 48 | #[inline] 49 | pub const fn from_git_at(url: Url, reference: Ref) -> Self { 50 | Source { 51 | url, 52 | kind: Kind::Git(reference), 53 | } 54 | } 55 | 56 | /// Where this source is located. 57 | #[inline] 58 | pub const fn url(&self) -> &Url { 59 | &self.url 60 | } 61 | 62 | /// The type of source. 63 | #[inline] 64 | pub const fn kind(&self) -> &Kind { 65 | &self.kind 66 | } 67 | } 68 | 69 | /// Determines how to treat a [`Source`](struct.Source.html). 70 | #[derive(Clone, Debug, PartialEq, Eq)] 71 | pub enum Kind { 72 | /// The drop is located in a git repository and the given reference should 73 | /// be used. 74 | Git(Ref), 75 | /// The drop is located in a registry. 76 | Registry, 77 | } 78 | -------------------------------------------------------------------------------- /shared/src/ext/process.rs: -------------------------------------------------------------------------------- 1 | use std::{io, process::Command}; 2 | 3 | /// Extended functionality for 4 | /// [`Command`](https://doc.rust-lang.org/std/process/struct.Command.html). 5 | pub trait CommandExt { 6 | /// Spawns `self`, replacing the calling program. 7 | /// 8 | /// Behavior: 9 | /// 10 | /// - Unix: simply calls [`exec`]. 11 | /// - Windows: sets the ctrl-c handler to always return `TRUE` (forwarding 12 | /// ctrl-c to the child), calls [`status`], and exits with the returned 13 | /// code. 14 | /// 15 | /// [`exec`]: https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#tymethod.exec 16 | /// [`status`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.status 17 | fn spawn_replace(&mut self) -> io::Error; 18 | } 19 | 20 | impl CommandExt for Command { 21 | #[cfg(unix)] 22 | #[inline] 23 | fn spawn_replace(&mut self) -> io::Error { 24 | use std::os::unix::process::CommandExt; 25 | self.exec() 26 | } 27 | 28 | #[cfg(windows)] 29 | #[inline] 30 | fn spawn_replace(&mut self) -> io::Error { 31 | use std::process::exit; 32 | use winapi::{ 33 | shared::minwindef::{BOOL, DWORD, TRUE}, 34 | um::consoleapi::SetConsoleCtrlHandler, 35 | }; 36 | 37 | unsafe extern "system" fn ctrlc_handler(_: DWORD) -> BOOL { 38 | // Do nothing; let the child process handle it. 39 | TRUE 40 | } 41 | 42 | unsafe { 43 | // TODO: Consider warning about this function failing. 44 | SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE); 45 | } 46 | 47 | match self.status() { 48 | Ok(status) => { 49 | let exit_code = match status.code() { 50 | Some(code) => code, 51 | None if status.success() => 0, 52 | None => 1, 53 | }; 54 | exit(exit_code); 55 | } 56 | Err(error) => error, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Ocean logo 6 | 7 |
8 |

Ocean Library

9 | 10 | Build Status 12 | 13 | 14 | Crates.io badge 16 | 17 |
18 |
19 | 20 | The `oceanpkg` library serves as core reusable components for: 21 | - The `ocean` [CLI] client 22 | - Backend web services 23 | 24 | 27 | **Note:** All shell commands assume that the current working directory is `lib`. 28 | This can be done by running `cd lib` to "change directory" from the root folder. 29 | 30 | ## Install 31 | 32 | This library is written in [Rust] and is meant to be used within a [`cargo`] 33 | project. See [rustup.rs] for installing Rust and `cargo`. 34 | 35 | It is made available [on crates.io][crate] and can be used by adding the 36 | following to your project's [`Cargo.toml`]: 37 | 38 | ```toml 39 | [dependencies] 40 | oceanpkg = "0.0.11" 41 | ``` 42 | 43 | and this to your crate root (`main.rs` or `lib.rs`): 44 | 45 | ```rust 46 | extern crate oceanpkg; 47 | ``` 48 | 49 | ## Usage 50 | 51 | See [documentation]. 52 | 53 | ## Testing 54 | 55 | Various test cases are covered throughout this library. They can all be found by 56 | searching for `mod tests` within the `lib` folder. 57 | 58 | To perform these tests, simply run: 59 | 60 | ```sh 61 | cargo test 62 | ``` 63 | 64 | [CLI]: https://en.wikipedia.org/wiki/Command-line_interface 65 | [Rust]: https://www.rust-lang.org 66 | [`cargo`]: https://doc.rust-lang.org/cargo 67 | [rustup.rs]: https://rustup.rs 68 | [crate]: https://crates.io/crates/oceanpkg 69 | [documentation]: https://docs.rs/oceanpkg 70 | [`Cargo.toml`]: https://doc.rust-lang.org/cargo/reference/manifest.html 71 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu-latest 13 | - windows-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | - uses: actions-rs/cargo@v1 22 | with: 23 | command: check 24 | 25 | test: 26 | name: Test Suite 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | rust: 31 | - stable 32 | - nightly 33 | os: 34 | - ubuntu-latest 35 | - windows-latest 36 | steps: 37 | - uses: actions/checkout@v1 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: ${{ matrix.rust }} 42 | override: true 43 | - uses: actions-rs/cargo@v1 44 | with: 45 | command: test 46 | 47 | fmt: 48 | name: Rustfmt 49 | runs-on: ${{ matrix.os }} 50 | strategy: 51 | matrix: 52 | os: 53 | - ubuntu-latest 54 | - windows-latest 55 | steps: 56 | - uses: actions/checkout@v1 57 | - uses: actions-rs/toolchain@v1 58 | with: 59 | profile: minimal 60 | toolchain: stable 61 | override: true 62 | - run: rustup component add rustfmt 63 | - uses: actions-rs/cargo@v1 64 | with: 65 | command: fmt 66 | args: --all -- --check 67 | 68 | clippy: 69 | name: Clippy 70 | runs-on: ${{ matrix.os }} 71 | strategy: 72 | matrix: 73 | os: 74 | - ubuntu-latest 75 | - windows-latest 76 | steps: 77 | - uses: actions/checkout@v1 78 | - uses: actions-rs/toolchain@v1 79 | with: 80 | profile: minimal 81 | toolchain: stable 82 | override: true 83 | - run: rustup component add clippy 84 | - uses: actions-rs/cargo@v1 85 | with: 86 | command: clippy 87 | args: -- -D warnings 88 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Ocean logo 6 | 7 |
8 |

Ocean Command-Line Interface

9 | 10 | Build Status 12 | 13 |
14 |
15 | 16 | The `ocean` [CLI] client is the main way of using Ocean. 17 | 18 | 21 | **Note:** All shell commands assume that the current working directory is `cli`. 22 | This can be done by running `cd cli` to "change directory" from the root folder. 23 | 24 | ## Build 25 | 26 | To build `ocean` without running it immediately, simply replace `run` with 27 | `build`: 28 | 29 | ```sh 30 | cargo build 31 | ``` 32 | 33 | This will generate a binary at `../target/debug/ocean`. 34 | 35 | To build with optimizations, add the `--release` flag. The binary will then be 36 | made available at `../target/release/ocean`. 37 | 38 | Notice that the default build folder is `../target`. To change this, use the 39 | `--target-dir` option. 40 | 41 | ## Run 42 | 43 | This client is written in [Rust] and is built with [`cargo`]. See [rustup.rs] 44 | for installing Rust and `cargo`. 45 | 46 | To run _without_ optimizations, run: 47 | 48 | ```sh 49 | cargo run 50 | ``` 51 | 52 | To build _with_ optimizations, run: 53 | 54 | ```sh 55 | cargo run --release 56 | ``` 57 | 58 | ### Run With Arguments 59 | 60 | Both of the above examples will simply output a help message and exit with a 61 | non-0 code. 62 | 63 | To pass arguments via `cargo`, place them after a lone `--`: 64 | 65 | ```sh 66 | cargo run -- install [FLAGS] [OPTIONS] ... 67 | ``` 68 | 69 | Otherwise, arguments can be passed to the compiled binary directly: 70 | 71 | ```sh 72 | ../target/debug/ocean install [FLAGS] [OPTIONS] ... 73 | ``` 74 | 75 | [CLI]: https://en.wikipedia.org/wiki/Command-line_interface 76 | [Rust]: https://www.rust-lang.org 77 | [`cargo`]: https://doc.rust-lang.org/cargo 78 | [rustup.rs]: https://rustup.rs 79 | [crate]: https://crates.io/crates/oceanpkg 80 | [`Cargo.toml`]: https://doc.rust-lang.org/cargo/reference/manifest.html 81 | -------------------------------------------------------------------------------- /cli/src/cmd/submit/bug.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use std::iter; 3 | 4 | /// The issue template as a macro so then it can be used as a formatting string 5 | /// separate from the rest of the code. 6 | /// 7 | /// See `.github/ISSUE_TEMPLATE/bug_report.md` for the base issue template. 8 | /// 9 | /// This *must* be small or else GitHub gives "414 Request-URI Too Large". 10 | macro_rules! template { 11 | () => { 12 | r#" 18 | 19 | # 🐛 Bug Report 20 | 21 | 24 | 25 | ## Steps to Reproduce 26 | 27 | 30 | 31 | ## Components Affected 32 | 33 | - CLI client 34 | - Version: {version} 35 | - Commit: {commit} 36 | 37 | ## Platforms Affected 38 | 39 | - {os} ({arch}) 40 | 41 | ## Detailed Description 42 | 43 | 46 | 47 | ## Relevant Issues 48 | 49 | "# 52 | }; 53 | } 54 | 55 | /// Creates a URL suitable for opening a condensed variant of [`bug_report.md`] 56 | /// with the user's Ocean and system info filled out. 57 | /// 58 | /// [`bug_report.md`]: https://github.com/oceanpkg/ocean/blob/master/.github/ISSUE_TEMPLATE/bug_report.md 59 | pub fn url(_config: &Config) -> String { 60 | let version = env!("CARGO_PKG_VERSION"); 61 | let revision = option_env!("OCEAN_GIT_REV").unwrap_or("Unknown"); 62 | 63 | let target_os = env!("OCEAN_TARGET_OS"); 64 | let target_arch = env!("OCEAN_TARGET_ARCH"); 65 | 66 | let body = format!( 67 | template!(), 68 | version = version, 69 | commit = revision, 70 | os = target_os, 71 | arch = target_arch, 72 | ); 73 | let base = "\ 74 | https://github.com/oceanpkg/ocean/issues/new\ 75 | ?assignees=nvzqz\ 76 | &template=bug_report.md\ 77 | &labels=kind%2Fbug\ 78 | &title=%5BDescribe+the+problem+you+encountered%5D\ 79 | &body=\ 80 | "; 81 | iter::once(base) 82 | .chain(percent_encoding::utf8_percent_encode( 83 | &body, 84 | percent_encoding::NON_ALPHANUMERIC, 85 | )) 86 | .collect() 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/config/user.rs: -------------------------------------------------------------------------------- 1 | //! User configuration data. 2 | 3 | use std::{collections::HashMap, ffi::OsStr, time::Duration}; 4 | 5 | /// Represents the configuration specific to the user. 6 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 7 | pub struct UserConfig { 8 | /// Whether to send logs to ensure correct behavior. 9 | pub send_logs: bool, 10 | /// How often to send logs if `send_logs` is `true`. The default is 1 week. 11 | pub send_logs_rate: Duration, 12 | /// Aliases for CLI commands. 13 | #[serde(rename = "alias")] 14 | pub aliases: HashMap, 15 | } 16 | 17 | impl Default for UserConfig { 18 | #[inline] 19 | fn default() -> Self { 20 | Self::new() 21 | } 22 | } 23 | 24 | impl UserConfig { 25 | /// Creates a new default configuration. 26 | #[inline] 27 | pub fn new() -> Self { 28 | const DAY_SECS: u64 = 86400; 29 | const WEEK_SECS: u64 = DAY_SECS * 7; 30 | Self { 31 | send_logs: false, 32 | send_logs_rate: Duration::from_secs(WEEK_SECS), 33 | aliases: HashMap::new(), 34 | } 35 | } 36 | 37 | /// Finds the command for `alias` and returns its parts as `&str`s. 38 | pub fn parse_alias<'a>(&'a self, alias: &str) -> Option> { 39 | match self.aliases.get(alias) { 40 | Some(Command::Unparsed(command)) => { 41 | Some(command.split_whitespace().collect()) 42 | } 43 | Some(Command::Parsed(command)) => { 44 | Some(command.iter().map(|s| s.as_str()).collect()) 45 | } 46 | None => None, 47 | } 48 | } 49 | 50 | /// Finds the command for `alias` and returns its parts as `&OsStr`s. 51 | pub fn parse_alias_os<'a>(&'a self, alias: &str) -> Option> { 52 | match self.aliases.get(alias) { 53 | Some(Command::Unparsed(command)) => { 54 | Some(command.split_whitespace().map(|s| s.as_ref()).collect()) 55 | } 56 | Some(Command::Parsed(command)) => { 57 | Some(command.iter().map(|s| s.as_ref()).collect()) 58 | } 59 | None => None, 60 | } 61 | } 62 | } 63 | 64 | /// A CLI command. 65 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 66 | #[serde(untagged)] 67 | pub enum Command { 68 | /// The raw command string. 69 | Unparsed(String), 70 | /// The command and its arguments as a list. 71 | Parsed(Vec), 72 | } 73 | -------------------------------------------------------------------------------- /cli/src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod prelude; 2 | use prelude::{App, ArgMatches, Config}; 3 | 4 | mod config; 5 | mod docs; 6 | mod home; 7 | mod install; 8 | mod list; 9 | mod login; 10 | mod new; 11 | mod package; 12 | mod run; 13 | mod search; 14 | #[path = "self/mod.rs"] 15 | mod self_; // `self` is a keyword 16 | mod ship; 17 | mod source; 18 | mod submit; 19 | mod uninstall; 20 | mod update; 21 | 22 | /// Returns all of Ocean's subcommands to pass into `App::subcommands`. 23 | pub fn all() -> Vec { 24 | vec![ 25 | list::cmd(), 26 | new::cmd(), 27 | search::cmd(), 28 | install::cmd(), 29 | uninstall::cmd(), 30 | update::cmd(), 31 | run::cmd(), 32 | config::cmd(), 33 | self_::cmd(), 34 | login::cmd(), 35 | ship::cmd(), 36 | package::cmd(), 37 | home::cmd(), 38 | docs::cmd(), 39 | source::cmd(), 40 | submit::cmd(), 41 | ] 42 | } 43 | 44 | /// A function suitable for running a subcommand with its own `ArgMatches`. 45 | pub type RunFn = fn(&mut Config, &ArgMatches) -> crate::Result; 46 | 47 | /// Returns the `run` function pointer for `subcommand`. 48 | pub fn builtin_run_fn(subcommand: &str) -> Option { 49 | #[rustfmt::skip] 50 | let run = match subcommand { 51 | list::NAME => list::run, 52 | new::NAME => new::run, 53 | search::NAME => search::run, 54 | install::NAME => install::run, 55 | uninstall::NAME => uninstall::run, 56 | update::NAME => update::run, 57 | run::NAME => run::run, 58 | config::NAME => config::run, 59 | self_::NAME => self_::run, 60 | login::NAME => login::run, 61 | ship::NAME => ship::run, 62 | package::NAME => package::run, 63 | home::NAME => home::run, 64 | docs::NAME => docs::run, 65 | source::NAME => source::run, 66 | submit::NAME => submit::run, 67 | _ => return None, 68 | }; 69 | Some(run) 70 | } 71 | 72 | /// Returns a pre-defined aliased command. 73 | pub fn builtin_alias(alias: &str) -> Option<&'static str> { 74 | // Default aliases should be for commands of 4 characters or more. 75 | // 76 | // Destructive commands like `uninstall` should never have a built-in alias. 77 | #[rustfmt::skip] 78 | let alias = match alias { 79 | "i" => install::NAME, 80 | "ls" => list::NAME, 81 | "s" => search::NAME, 82 | _ => return None, 83 | }; 84 | Some(alias) 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/system.rs: -------------------------------------------------------------------------------- 1 | //! System utilities. 2 | 3 | use std::{ 4 | ffi::OsStr, 5 | io, 6 | process::{Child, Command, ExitStatus}, 7 | }; 8 | 9 | /// The tool for opening resources in the user's preferred application. 10 | /// 11 | /// | Platform | Value | 12 | /// | :------- | :---- | 13 | /// | Linux | `xdg-open` | 14 | /// | macOS | `/usr/bin/open | 15 | /// | Windows | `start` | 16 | #[allow(clippy::let_and_return)] 17 | pub const OPEN_TOOL: &str = { 18 | cfg_if! { 19 | if #[cfg(target_os = "macos")] { 20 | let open = "/usr/bin/open"; 21 | } else if #[cfg(target_os = "linux")] { 22 | let open = "xdg-open"; 23 | } else if #[cfg(target_os = "windows")] { 24 | let open = "start"; 25 | } else { 26 | compile_error!( 27 | "No known way of opening a resource on the current platform." 28 | ); 29 | } 30 | }; 31 | open 32 | }; 33 | 34 | /// Opens `resources` in the user's preferred application. 35 | pub fn open(resources: &[R]) -> io::Result<()> 36 | where 37 | R: AsRef, 38 | { 39 | fn status_to_result(status: ExitStatus) -> io::Result<()> { 40 | if status.success() { 41 | Ok(()) 42 | } else { 43 | Err(io::Error::new( 44 | io::ErrorKind::Other, 45 | format!("Opening finished with status {}", status), 46 | )) 47 | } 48 | } 49 | 50 | // Do nothing if no resources were provided. 51 | if resources.is_empty() { 52 | return Ok(()); 53 | } 54 | 55 | if cfg!(target_os = "macos") { 56 | // The tool supports providing multiple resources at once. 57 | let mut cmd = Command::new(OPEN_TOOL); 58 | cmd.args(resources); 59 | status_to_result(cmd.status()?) 60 | } else { 61 | // The tool needs to be spawned multiple times in order to open multiple 62 | // resources. 63 | 64 | // Spawn all processes before attempting to get their exit statuses. 65 | // This ensures we don't fail early if a single resource couldn't be 66 | // opened for any reason. 67 | let children: Vec> = resources 68 | .iter() 69 | .map(|res| { 70 | let mut cmd = Command::new(OPEN_TOOL); 71 | cmd.arg(res); 72 | cmd.spawn() 73 | }) 74 | .collect(); 75 | 76 | for child in children { 77 | status_to_result(child?.wait()?)?; 78 | } 79 | 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cli/src/cmd/ship.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::auth::Credentials; 3 | use std::{ 4 | fs::File, 5 | io::{self, Read}, 6 | path::Path, 7 | }; 8 | 9 | pub const NAME: &str = "ship"; 10 | 11 | pub fn cmd() -> App { 12 | SubCommand::with_name(NAME) 13 | .about("Package and upload this drop to the registry") 14 | .arg( 15 | Arg::with_name("token") 16 | .help("Token to use when uploading") 17 | .long("token") 18 | .takes_value(true), 19 | ) 20 | .arg( 21 | Arg::with_name("manifest") 22 | .help("Path to Ocean.toml") 23 | .long("manifest") 24 | .takes_value(true), 25 | ) 26 | } 27 | 28 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 29 | let package = oceanpkg::drop::Package::create( 30 | config.rt.current_dir(), 31 | matches.value_of_os("manifest"), 32 | None::<&Path>, 33 | )?; 34 | 35 | let credentials: String; 36 | let token = match matches.value_of("token") { 37 | Some(token) => token, 38 | None => { 39 | let credentials_path = config.rt.credentials_path(); 40 | 41 | let mut credentials_file = match File::open(credentials_path) { 42 | Ok(file) => file, 43 | Err(error) => { 44 | use io::ErrorKind; 45 | match error.kind() { 46 | ErrorKind::NotFound => { 47 | failure::bail!("please run `ocean login` first") 48 | } 49 | _ => return Err(error.into()), 50 | } 51 | } 52 | }; 53 | 54 | let mut credentials_buf = String::with_capacity(256); 55 | credentials_file.read_to_string(&mut credentials_buf)?; 56 | credentials = credentials_buf; 57 | 58 | let credentials: Credentials<&str> = toml::from_str(&credentials)?; 59 | credentials 60 | .registry 61 | .ok_or_else(|| { 62 | failure::err_msg("please run `ocean login` first") 63 | })? 64 | .token 65 | } 66 | }; 67 | 68 | oceanpkg::api::v1::ship(&package, token)?; 69 | 70 | // Get duration immediately after shipping finishes. 71 | let elapsed = config.rt.time_elapsed(); 72 | 73 | println!("Successfully shipped \"{}\"!", package.manifest.meta.name); 74 | println!("Finished in {:?}", elapsed); 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Something is rotten in the state of Denmark... 🤔 4 | title: "[Describe the problem you encountered]" 5 | labels: kind/bug 6 | assignees: nvzqz 7 | 8 | --- 9 | 10 | 16 | 17 | # 🐛 Bug Report 18 | 19 | 22 | 23 | I did ... 24 | 25 | I expected ... to happen. 26 | 27 | Instead I got ... 28 | 29 | ## Steps to Reproduce 30 | 31 | 39 | 40 | 1. Go to ... 41 | 2. Click on ... 42 | 3. Scroll down to ... 43 | 4. See ... 44 | 45 | ## Components Affected 46 | 47 | 54 | 55 | 56 | - CLI client 57 | - Version: 0.1 58 | 59 | 64 | 65 | ## Platforms Affected 66 | 67 | 74 | 75 | 76 | Windows 7, 32-bit 77 | 78 | ## Environment Variables 79 | 80 | 83 | 84 | 85 | ```txt 86 | OCEAN_API_URL=https://api.oceanpkg.org/ 87 | ``` 88 | 89 | ## Detailed Description 90 | 91 | 94 | 95 | There is an issue where ... 96 | 97 | I tried this code/command: ... 98 | 99 | I expected this to happen: ... 100 | 101 | Instead, this happened: ... 102 | 103 | ## Relevant Issues 104 | 105 | 108 | 109 | Unknown 110 | -------------------------------------------------------------------------------- /lib/src/drop/version.rs: -------------------------------------------------------------------------------- 1 | //! Versioning schemes. 2 | 3 | use serde::{Serialize, Serializer}; 4 | use std::fmt; 5 | 6 | #[doc(inline)] 7 | pub use semver::Version as SemVer; 8 | 9 | flexible! { 10 | /// A drop version. 11 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 12 | pub enum Version { 13 | /// [Semantic versioning](http://semver.org). This is the default. 14 | #[serde(rename = "semver")] 15 | SemVer(SemVer), 16 | /// A custom versioning scheme. 17 | #[serde(rename = "custom")] 18 | Custom(String), 19 | } 20 | } 21 | 22 | impl From for Version { 23 | #[inline] 24 | fn from(v: SemVer) -> Self { 25 | Version::SemVer(v) 26 | } 27 | } 28 | 29 | impl PartialEq for Version { 30 | fn eq(&self, s: &str) -> bool { 31 | match self { 32 | // TODO: Switch to a `SemVer` type that supports string equality 33 | // without doing full parsing 34 | Self::SemVer(v) => Ok(v) == SemVer::parse(s).as_ref(), 35 | Self::Custom(v) => v == s, 36 | } 37 | } 38 | } 39 | 40 | impl fmt::Display for Version { 41 | #[inline] 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | match self { 44 | Self::SemVer(v) => v.fmt(f), 45 | Self::Custom(v) => v.fmt(f), 46 | } 47 | } 48 | } 49 | 50 | impl Serialize for Version { 51 | fn serialize(&self, ser: S) -> Result 52 | where 53 | S: Serializer, 54 | { 55 | use serde::ser::SerializeMap; 56 | 57 | match self { 58 | Version::SemVer(semver) => ser.collect_str(semver), 59 | Version::Custom(custom) => { 60 | let mut map = ser.serialize_map(Some(1))?; 61 | map.serialize_entry("custom", custom)?; 62 | map.end() 63 | } 64 | } 65 | } 66 | } 67 | 68 | impl Version { 69 | /// Creates a new instance from a custom `version`. 70 | #[inline] 71 | pub fn custom(version: V) -> Self 72 | where 73 | V: Into, 74 | { 75 | Self::Custom(version.into()) 76 | } 77 | 78 | /// Attempts to parse `version` as SemVer. 79 | #[inline] 80 | pub fn parse_semver(version: &str) -> Result { 81 | SemVer::parse(version).map(Self::SemVer) 82 | } 83 | 84 | /// Returns the name of the version kind: `semver` or `custom`. 85 | #[inline] 86 | pub fn kind(&self) -> &'static str { 87 | match self { 88 | Version::SemVer(_) => "semver", 89 | Version::Custom(_) => "custom", 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /shared/src/ext/bytes.rs: -------------------------------------------------------------------------------- 1 | /// Extended functionality for 2 | /// [`&[u8]`](https://doc.rust-lang.org/std/primitive.slice.html). 3 | pub trait BytesExt { 4 | /// Returns whether `self` matches case-insensitively to `other`, which only 5 | /// contains [ASCII] characters with the `0b100000` (lowercase) bit set. 6 | /// 7 | /// This method can often be used in place of 8 | /// [`eq_ignore_ascii_case`](https://doc.rust-lang.org/std/primitive.slice.html#method.eq_ignore_ascii_case) 9 | /// and is more performant since this uses a simple bitwise OR instead of a 10 | /// lookup table. The main restriction is that only the following [ASCII] 11 | /// characters may be in `other`: 12 | /// 13 | /// | Type | Values | 14 | /// | :------------ | :----- | 15 | /// | Alphanumeric | `a`-`z`, `0`-`9` | 16 | /// | Punctuation | `!`, `?`, `.`, `,`, `:`, `;`, `'`, `` ` ``, `\`, `/`, `#`, `$`, `&`, |, `~` | 17 | /// | Brackets | `<`, `>`, `(`, `)`, `{`, `}` | 18 | /// | Math | `+`, `-`, `*`, `%`, `=` | 19 | /// | Non-Graphical | `SPACE`, `DELETE` | 20 | /// 21 | /// # Examples 22 | /// 23 | /// This method can be used to match against filesystem paths: 24 | /// 25 | /// ```rust 26 | /// use oceanpkg_shared::ext::BytesExt; 27 | /// 28 | /// let lower = b"../hello.txt"; 29 | /// let upper = b"../HELLO.TXT"; 30 | /// 31 | /// assert!(upper.matches_special_lowercase(lower)); 32 | /// assert!(lower.matches_special_lowercase(lower)); 33 | /// assert!(!lower.matches_special_lowercase(upper)); 34 | /// assert!(!upper.matches_special_lowercase(upper)); 35 | /// ``` 36 | /// 37 | /// [ASCII]: https://en.wikipedia.org/wiki/ASCII 38 | fn matches_special_lowercase>(self, other: B) -> bool; 39 | } 40 | 41 | // Monomorphized form 42 | fn matches_special_lowercase_imp(a: &[u8], b: &[u8]) -> bool { 43 | a.len() == b.len() && a.iter().zip(b).all(|(&a, &b)| a | 0b10_0000 == b) 44 | } 45 | 46 | impl BytesExt for &[u8] { 47 | fn matches_special_lowercase>(self, other: B) -> bool { 48 | matches_special_lowercase_imp(self, other.as_ref()) 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::*; 55 | 56 | #[test] 57 | fn matches_special_lowercase() { 58 | let cases = [ 59 | (["ocean.toml", "ocean.toml"], true), 60 | (["OCEAN.toMl", "ocean.toml"], true), 61 | (["ocean.toml", "OCEAN.toml"], false), 62 | (["ocean.tom", "ocean.toml"], false), 63 | ]; 64 | for &([a, b], cond) in cases.iter() { 65 | assert_eq!(a.as_bytes().matches_special_lowercase(b), cond); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cli/src/cmd/config/mod.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::config::file::*; 3 | use std::{env, fs, io, process}; 4 | 5 | pub const NAME: &str = "config"; 6 | 7 | pub fn cmd() -> App { 8 | SubCommand::with_name(NAME) 9 | .about("Configure the settings used by Ocean") 10 | .arg( 11 | Arg::global_flag() 12 | .help("The global configuration file for all users"), 13 | ) 14 | } 15 | 16 | pub fn run(_state: &mut Config, matches: &ArgMatches) -> crate::Result { 17 | let install_target = matches.install_target(); 18 | 19 | let config_dir = match install_target.cfg_dir() { 20 | Ok(dir) => dir, 21 | Err(err) => { 22 | unimplemented!("{}", err); 23 | } 24 | }; 25 | 26 | if !config_dir.exists() { 27 | if let Err(err) = fs::create_dir(&config_dir) { 28 | match err.kind() { 29 | io::ErrorKind::PermissionDenied => { 30 | panic!("Permission to create {:?} denied", config_dir); 31 | } 32 | _ => panic!("{}", err), 33 | } 34 | } 35 | } 36 | 37 | if !config_dir.is_dir() { 38 | panic!("Found non-directory file at {:?}", config_dir); 39 | } 40 | 41 | let config_file = match ConfigFile::find(&config_dir) { 42 | Ok(file) => file, 43 | Err(err) => match err.reason { 44 | NotFoundReason::Io(err) => unimplemented!("{}", err), 45 | NotFoundReason::NoMatch => { 46 | let path = config_dir.join("ocean.toml"); 47 | let file = match fs::File::create(&path) { 48 | Ok(file) => file, 49 | Err(err) => match err.kind() { 50 | io::ErrorKind::PermissionDenied => { 51 | panic!("Permission to create {:?} denied", path); 52 | } 53 | _ => panic!("{}", err), 54 | }, 55 | }; 56 | ConfigFile { 57 | path, 58 | fmt: ConfigFileFmt::Toml, 59 | handle: Some(file), 60 | } 61 | } 62 | }, 63 | }; 64 | 65 | println!("Found config file: {:#?}", config_file); 66 | 67 | #[cfg(unix)] 68 | let editor = if let Some(editor) = env::var_os("VISUAL") { 69 | editor 70 | } else if let Some(editor) = env::var_os("EDITOR") { 71 | editor 72 | } else { 73 | panic!("Could not determine editor"); 74 | }; 75 | 76 | #[cfg(windows)] 77 | let editor: ffi::OsString = unimplemented!(); 78 | 79 | process::Command::new(editor) 80 | .arg(&config_file.path) 81 | .spawn()?; 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/drop/manifest/meta.rs: -------------------------------------------------------------------------------- 1 | use crate::drop::{source::Git, version::SemVer}; 2 | use std::collections::BTreeMap; 3 | 4 | /// The value for the `meta` key in the drop manifest. 5 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 6 | #[serde(rename_all = "kebab-case")] 7 | pub struct Meta { 8 | /// The drop's name. 9 | pub name: String, 10 | 11 | /// The pretty name displayed when viewing a drop. 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub display_name: Option, 14 | 15 | /// What is this drop? 16 | pub description: String, 17 | 18 | /// The path of the executable. `name` is used if `None`. 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub exe_path: Option, 21 | 22 | /// The licenses used. 23 | /// 24 | /// This can be a single license or multiple delimited by "AND" or "OR". 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub license: Option, 27 | 28 | /// Authors of the drop. 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub authors: Option>, 31 | 32 | /// A path to the package's "README" file. 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub readme: Option, 35 | 36 | /// A path to the package's change log file. 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub changelog: Option, 39 | 40 | /// This drop's corner of the internet. 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub homepage: Option, 43 | 44 | /// The URL where docs live. 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub documentation: Option, 47 | 48 | /// The drop version. 49 | // TODO: Switch to a flexible versioning scheme that can parse `SemVer` with 50 | // any number of dots. If not `SemVer`, call it `Custom` and look into other 51 | // versioning schemes. 52 | // TODO: Consider accepting dates? 53 | pub version: SemVer, 54 | 55 | // Tables: all types that serialize into maps (or "tables" in TOML) 56 | // them must be placed last to succeed. 57 | /// The git repository where this drop can be fetched from. 58 | /// 59 | /// Repository info is taKen from here. 60 | #[serde(skip_serializing_if = "Option::is_none")] 61 | pub git: Option, 62 | 63 | /// The versions that this version conflicts with. 64 | #[serde(skip_serializing_if = "Option::is_none")] 65 | pub conflicts: Option>, 66 | } 67 | 68 | impl Meta { 69 | /// Returns the path where the executable is expected to be. 70 | pub fn exe_path(&self) -> &str { 71 | match &self.exe_path { 72 | Some(path) => path, 73 | None => &self.name, 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cli/src/cmd/submit/feature.rs: -------------------------------------------------------------------------------- 1 | use super::super::prelude::*; 2 | use std::iter; 3 | 4 | /// See `.github/ISSUE_TEMPLATE/feature_request.md` for the base issue template. 5 | /// 6 | /// This *must* be small or else GitHub gives "414 Request-URI Too Large". 7 | const TEMPLATE: &str = r#" 10 | 11 | # 💡 Feature Request 12 | 13 | 16 | 17 | I have a dream that Ocean will be able to ... 18 | 19 | ## Components 20 | 21 | 28 | 29 | This would work on Ocean's ... 30 | 31 | ## Motivation 32 | 33 | 38 | 39 | We should do this because ... 40 | 41 | ## Proposal 42 | 43 | 47 | 48 | This can be achieved by ... 49 | 50 | ## Prior Art 51 | 52 | 59 | 60 | If you take a look at ..., you'll see that it has ... 61 | 62 | ## Alternatives 63 | 64 | 69 | 70 | We could also take the approach explained at ... 71 | "#; 72 | 73 | /// Creates a URL suitable for opening a condensed variant of [`bug_report.md`] 74 | /// with the user's Ocean and system info filled out. 75 | /// 76 | /// [`bug_report.md`]: https://github.com/oceanpkg/ocean/blob/master/.github/ISSUE_TEMPLATE/bug_report.md 77 | pub fn url(_config: &Config) -> String { 78 | let body = TEMPLATE; 79 | let base = "\ 80 | https://github.com/oceanpkg/ocean/issues/new\ 81 | ?assignees=nvzqz\ 82 | &template=feature_request.md\ 83 | &labels=kind%2Ffeature\ 84 | &title=%5BDescribe+the+feature+you%27re+proposing%5D\ 85 | &body=\ 86 | "; 87 | iter::once(base) 88 | .chain(percent_encoding::utf8_percent_encode( 89 | &body, 90 | percent_encoding::NON_ALPHANUMERIC, 91 | )) 92 | .collect() 93 | } 94 | -------------------------------------------------------------------------------- /cli/src/cmd/run.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::{ 3 | drop::{name::Query, Manifest}, 4 | install::InstallTarget, 5 | }; 6 | use std::process::{exit, Command}; 7 | 8 | pub const NAME: &str = "run"; 9 | 10 | const AFTER_HELP: &str = "\ 11 | All arguments following the two dashes (`--`) are passed directly to the drop \ 12 | as its own arguments. Any arguments designated for Ocean should go before the \ 13 | `--`. 14 | 15 | Example: 16 | 17 | $ ocean run wget -- https://example.com 18 | "; 19 | 20 | pub fn cmd() -> App { 21 | SubCommand::with_name(NAME) 22 | .about("Executes a drop") 23 | .settings(&[ 24 | AppSettings::ArgRequiredElseHelp, 25 | AppSettings::TrailingVarArg, 26 | ]) 27 | .arg( 28 | Arg::global_flag() 29 | .help("Execute the drop that's available to all users"), 30 | ) 31 | .arg( 32 | Arg::with_name("drop") 33 | .help("Name of the target drop to run") 34 | .required(true), 35 | ) 36 | .arg( 37 | Arg::with_name("args") 38 | .help("Arguments passed directly to the drop") 39 | .last(true) 40 | .multiple(true), 41 | ) 42 | .after_help(AFTER_HELP) 43 | } 44 | 45 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 46 | if let Some(drop) = matches.value_of("drop") { 47 | let query = Query::<&str>::parse_liberal(drop); 48 | 49 | let scope = query.scope.unwrap_or("core"); 50 | let name = query.name; 51 | let version = query.version.ok_or_else(|| { 52 | failure::err_msg("Provided query must include a version") 53 | })?; 54 | 55 | let query_string = format!("{}/{}@{}", scope, name, version); 56 | 57 | let drops_dir = config.rt.drops_dir(&InstallTarget::CurrentUser); 58 | let drop_path = drops_dir.join(&query_string); 59 | 60 | let manifest_path = drop_path.join(Manifest::FILE_NAME); 61 | if !manifest_path.exists() { 62 | failure::bail!( 63 | "Could not run \"{}\"; please install it", 64 | query_string 65 | ); 66 | } 67 | 68 | let manifest = Manifest::read_toml_file(&manifest_path)?; 69 | 70 | let exe_path = drop_path.join(manifest.meta.exe_path()); 71 | 72 | let mut cmd = Command::new(&exe_path); 73 | if let Some(args) = matches.values_of("args") { 74 | cmd.args(args); 75 | } 76 | 77 | let status = cmd.status()?; 78 | let code = if status.success() { 79 | status.code().unwrap_or(0) 80 | } else { 81 | status.code().unwrap_or(1) 82 | }; 83 | exit(code); 84 | } else { 85 | // ArgRequiredElseHelp 86 | } 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/drop/mod.rs: -------------------------------------------------------------------------------- 1 | //! Ocean packages, also known as drops 💧. 2 | 3 | #[cfg(feature = "toml")] 4 | mod package; 5 | 6 | pub mod kind; 7 | pub mod license; 8 | pub mod manifest; 9 | pub mod name; 10 | pub mod source; 11 | pub mod version; 12 | 13 | use self::kind::{App, Exe, Font, Lib}; 14 | 15 | #[doc(inline)] 16 | pub use self::{ 17 | kind::Kind, license::License, manifest::Manifest, name::Name, 18 | source::Source, version::Version, 19 | }; 20 | 21 | #[cfg(feature = "toml")] 22 | #[doc(inline)] 23 | pub use self::package::*; 24 | 25 | /// Defines an Ocean package, also known as a drop 💧. 26 | #[derive(Clone, Debug)] 27 | pub enum Drop { 28 | /// A package with a graphical interface. 29 | App(App), 30 | /// A package that can be executed; e.g. CLI tool or script. 31 | Exe(Exe), 32 | /// A package for a typeface with specific properties; e.g. bold, italic. 33 | Font(Font), 34 | /// A package for a library of a given language. 35 | Lib(Lib), 36 | } 37 | 38 | impl From for Drop { 39 | #[inline] 40 | fn from(drop: App) -> Self { 41 | Self::App(drop) 42 | } 43 | } 44 | 45 | impl From for Drop { 46 | #[inline] 47 | fn from(drop: Exe) -> Self { 48 | Self::Exe(drop) 49 | } 50 | } 51 | 52 | impl From for Drop { 53 | #[inline] 54 | fn from(drop: Font) -> Self { 55 | Self::Font(drop) 56 | } 57 | } 58 | 59 | impl From for Drop { 60 | #[inline] 61 | fn from(drop: Lib) -> Self { 62 | Self::Lib(drop) 63 | } 64 | } 65 | 66 | impl Drop { 67 | /// 68 | pub fn query(query: &name::Query) -> Result 69 | where 70 | S: AsRef, 71 | { 72 | let query = query.to_ref::(); 73 | unimplemented!("TODO: Find '{}' drop", query); 74 | } 75 | 76 | /// Returns the kind of drop. 77 | pub fn kind(&self) -> Kind { 78 | match self { 79 | Self::App(_) => Kind::App, 80 | Self::Exe(_) => Kind::Exe, 81 | Self::Font(_) => Kind::Font, 82 | Self::Lib(_) => Kind::Lib, 83 | } 84 | } 85 | 86 | /// Returns basic metadata for the drop. 87 | pub fn metadata(&self) -> &Metadata { 88 | match self { 89 | Self::App(app) => app.metadata(), 90 | Self::Exe(exe) => exe.metadata(), 91 | Self::Font(font) => font.metadata(), 92 | Self::Lib(lib) => lib.metadata(), 93 | } 94 | } 95 | } 96 | 97 | /// Information common to all drops. 98 | #[derive(Clone, Debug)] 99 | pub struct Metadata { 100 | // TODO: Replace `scope` and `name` with a `ScopedNameRef` 101 | /// The drop's namespace. 102 | pub scope: String, 103 | /// The drop's unique name within its namespace. 104 | pub name: String, 105 | } 106 | -------------------------------------------------------------------------------- /cli/src/cli/exec.rs: -------------------------------------------------------------------------------- 1 | use oceanpkg::Config; 2 | use oceanpkg_shared::ext::*; 3 | use std::{ 4 | env::{self, consts::EXE_SUFFIX}, 5 | fs, 6 | path::{Path, PathBuf}, 7 | process::Command, 8 | }; 9 | 10 | pub fn run_external( 11 | config: &Config, 12 | cmd: &str, 13 | args: &clap::ArgMatches, 14 | ) -> crate::Result { 15 | use oceanpkg::env::*; 16 | 17 | let bin_dir = config.rt.bin_dir(); 18 | let mut search_dirs = vec![bin_dir.clone()]; 19 | 20 | if let Ok(path) = config.rt.path_var() { 21 | search_dirs.extend(env::split_paths(path)); 22 | } 23 | 24 | let command_exe = find_executable(cmd, search_dirs).ok_or_else(|| { 25 | // TODO: List the possible subcommands and suggest a close match. 26 | failure::format_err!("No such subcommand: {}", cmd) 27 | })?; 28 | 29 | let mut command = Command::new(command_exe); 30 | 31 | if let Some(args) = args.values_of_os("") { 32 | command.args(args); 33 | } 34 | 35 | if let Ok(ocean) = config.rt.current_exe() { 36 | command.env(OCEAN, ocean); 37 | } 38 | command.env(OCEAN_BIN_DIR, &bin_dir); 39 | command.env(OCEAN_VERSION, env!("CARGO_PKG_VERSION")); 40 | 41 | // TODO: Turn into a custom error with a better error message. 42 | let error = command.spawn_replace(); 43 | 44 | return Err(error.into()); 45 | } 46 | 47 | #[cfg(unix)] 48 | fn find_executable(cmd: &str, dirs: Vec) -> Option { 49 | find_executable_simple(cmd, dirs) 50 | } 51 | 52 | #[cfg(windows)] 53 | fn find_executable(cmd: &str, dirs: Vec) -> Option { 54 | use std::ffi::OsString; 55 | 56 | if let Some(path_ext) = env::var_os("PATHEXT") { 57 | for ext in env::split_paths(&path_ext) { 58 | let mut file = OsString::from(format!("ocean-{}", cmd)); 59 | file.push(ext); 60 | 61 | for dir in &dirs { 62 | let file = dir.join(&file); 63 | if is_executable(&file) { 64 | return Some(file); 65 | } 66 | } 67 | } 68 | None 69 | } else { 70 | find_executable_simple(cmd, dirs) 71 | } 72 | } 73 | 74 | fn find_executable_simple(cmd: &str, dirs: Vec) -> Option { 75 | let file_name = format!("ocean-{}{}", cmd, EXE_SUFFIX); 76 | dirs.into_iter() 77 | .map(|dir| dir.pushing(&file_name)) 78 | .find(|file| is_executable(file)) 79 | } 80 | 81 | #[cfg(unix)] 82 | fn is_executable(path: &Path) -> bool { 83 | use std::os::unix::prelude::*; 84 | fs::metadata(path) 85 | .map(|metadata| { 86 | metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 87 | }) 88 | .unwrap_or(false) 89 | } 90 | 91 | #[cfg(windows)] 92 | fn is_executable(path: &Path) -> bool { 93 | fs::metadata(path) 94 | .map(|metadata| metadata.is_file()) 95 | .unwrap_or(false) 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/drop/license/mod.rs: -------------------------------------------------------------------------------- 1 | //! Package licensing. 2 | //! 3 | //! **Note:** this module re-exports all of [`linfo`](https://docs.rs/linfo). 4 | 5 | use std::{borrow::Cow, fmt}; 6 | 7 | mod serde; 8 | 9 | // BUG(docs): `Expr` and `SpdxLicense` don't get rendered despite the glob. They 10 | // would, however, still be available within this module with just the glob. 11 | #[doc(inline)] 12 | pub use linfo::{Expr, SpdxLicense, *}; 13 | 14 | /// Any license, known or otherwise. 15 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 16 | pub enum AnyLicense<'a> { 17 | /// A license known to Ocean. 18 | /// 19 | /// This means information such as OSI approval and "libre"-ness can be 20 | /// checked. 21 | Known(License), 22 | /// A license unknown to Ocean. This is generally treated as an opaque ID. 23 | Unknown(Cow<'a, str>), 24 | } 25 | 26 | impl From for AnyLicense<'_> { 27 | #[inline] 28 | fn from(known: License) -> Self { 29 | Self::Known(known) 30 | } 31 | } 32 | 33 | impl From for AnyLicense<'_> { 34 | #[inline] 35 | fn from(spdx: SpdxLicense) -> Self { 36 | Self::Known(spdx.into()) 37 | } 38 | } 39 | 40 | impl<'a> From<&'a str> for AnyLicense<'a> { 41 | #[inline] 42 | fn from(s: &'a str) -> Self { 43 | if let Ok(l) = License::parse(s) { 44 | Self::Known(l) 45 | } else { 46 | Self::Unknown(Cow::Borrowed(s)) 47 | } 48 | } 49 | } 50 | 51 | impl From for AnyLicense<'_> { 52 | #[inline] 53 | fn from(s: String) -> Self { 54 | Cow::<'_, str>::Owned(s).into() 55 | } 56 | } 57 | 58 | impl<'a> From> for AnyLicense<'a> { 59 | #[inline] 60 | fn from(s: Cow<'a, str>) -> Self { 61 | if let Ok(l) = License::parse(s.as_ref()) { 62 | Self::Known(l) 63 | } else { 64 | Self::Unknown(s) 65 | } 66 | } 67 | } 68 | 69 | impl fmt::Display for AnyLicense<'_> { 70 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 71 | match self { 72 | Self::Known(known) => known.fmt(f), 73 | Self::Unknown(unknown) => unknown.fmt(f), 74 | } 75 | } 76 | } 77 | 78 | impl<'a> AnyLicense<'a> { 79 | /// Creates an instance from `s` where any external reference in `s` is not 80 | /// kept. 81 | #[inline] 82 | pub fn owned(s: S) -> Self 83 | where 84 | S: Into + AsRef, 85 | { 86 | if let Ok(l) = License::parse(s.as_ref()) { 87 | Self::Known(l) 88 | } else { 89 | Self::Unknown(Cow::Owned(s.into())) 90 | } 91 | } 92 | 93 | /// Returns the license's identifier by reference. 94 | #[inline] 95 | pub fn id(&self) -> &str { 96 | match self { 97 | Self::Known(l) => l.id(), 98 | Self::Unknown(id) => id, 99 | } 100 | } 101 | 102 | /// Returns whether the license is known to Ocean. 103 | #[inline] 104 | pub fn is_known(&self) -> bool { 105 | match self { 106 | Self::Known(_) => true, 107 | Self::Unknown(_) => false, 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/api/v1/download.rs: -------------------------------------------------------------------------------- 1 | use crate::{api, drop::name::Query}; 2 | use std::io; 3 | 4 | /// Requests the archive for a drop that matches `query` from [`url`]. 5 | /// 6 | /// [`url`]: fn.url.html 7 | pub fn download_drop( 8 | query: Query<&str>, 9 | writer: &mut dyn io::Write, 10 | ) -> Result<(), DownloadError> { 11 | let url = api::url()?; 12 | download_drop_at(&url, query, writer) 13 | } 14 | 15 | /// Requests the archive for a drop that matches `query` from a base API URL. 16 | /// 17 | /// This mainly exists so that we can also issue requests to testing and staging 18 | /// environments. 19 | pub fn download_drop_at( 20 | api_url: &url::Url, 21 | query: Query<&str>, 22 | writer: &mut dyn io::Write, 23 | ) -> Result<(), DownloadError> { 24 | let url = query.join_to_url(&api_url.join("/v1/")?)?; 25 | download_drop_at_specific(url.as_str(), writer) 26 | } 27 | 28 | /// Requests an API login token from a specific URL. 29 | pub fn download_drop_at_specific( 30 | url: U, 31 | writer: &mut dyn io::Write, 32 | ) -> Result<(), DownloadError> { 33 | // Monomorphized body to slightly reduce the instruction count of the 34 | // binary. 35 | fn download_drop( 36 | builder: reqwest::RequestBuilder, 37 | writer: &mut dyn io::Write, 38 | ) -> Result<(), DownloadError> { 39 | let mut response = builder.send()?; 40 | 41 | let status = response.status(); 42 | if !status.is_success() { 43 | return Err(DownloadError::from(status)); 44 | } 45 | 46 | response.copy_to(writer)?; 47 | 48 | Ok(()) 49 | } 50 | 51 | download_drop(reqwest::Client::new().get(url), writer) 52 | } 53 | 54 | /// An error returned when attempting to log into Ocean's API. 55 | #[derive(Debug)] 56 | pub enum DownloadError { 57 | /// Failed to parse a `Url`. 58 | ParseUrl(url::ParseError), 59 | /// Failed to send the request via `reqwest`. 60 | Request(reqwest::Error), 61 | /// Received an error status code. 62 | Status(http::StatusCode), 63 | /// Failed to write the response. 64 | Io(io::Error), 65 | } 66 | 67 | impl From for DownloadError { 68 | fn from(error: url::ParseError) -> Self { 69 | Self::ParseUrl(error) 70 | } 71 | } 72 | 73 | impl From for DownloadError { 74 | fn from(error: reqwest::Error) -> Self { 75 | Self::Request(error) 76 | } 77 | } 78 | 79 | impl From for DownloadError { 80 | fn from(error: http::StatusCode) -> Self { 81 | Self::Status(error) 82 | } 83 | } 84 | 85 | impl From for DownloadError { 86 | fn from(error: io::Error) -> Self { 87 | Self::Io(error) 88 | } 89 | } 90 | 91 | impl std::fmt::Display for DownloadError { 92 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 93 | use self::DownloadError::*; 94 | match self { 95 | ParseUrl(error) => error.fmt(f), 96 | Request(error) => error.fmt(f), 97 | Status(code) => write!(f, "received response \"{}\"", code), 98 | Io(error) => write!(f, "failed to write response: {}", error), 99 | } 100 | } 101 | } 102 | 103 | impl std::error::Error for DownloadError {} 104 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ocean 2 | 3 | Thank you for taking the time to help improve Ocean! We are excited to have you 4 | on board! 🛳 5 | 6 | There are several opportunities to contribute to Ocean at any depth. It doesn't 7 | matter if you are just getting started or know what cursed code lies deep below, 8 | we can use your help! 9 | 10 | _All contributions_ are **valued** and _no contribution_ is **too small**. 11 | 12 | The guide below will help you get your feet wet. Do not let this guide 13 | intimidate you. Consider it a map to help you navigate Ocean, whether you're 14 | dipping your toes or diving deep into the trenches. 15 | 16 | ## Index 17 | 18 | - [Conduct](#conduct) 19 | - [Contributing in Issues](#contributing-in-issues) 20 | - [Feature Requests](#feature-requests) 21 | - [Bug Reports](#bug-reports) 22 | - [Contributing in Pull Requests](#contributing-in-pull-requests) 23 | - [License and Copyright](#license-and-copyright) 24 | 25 | ## Conduct 26 | 27 | The [Ocean Code of Conduct] describes the _minimum_ behavior expected from all 28 | contributors. 29 | 30 | ## Contributing in Issues 31 | 32 | Ocean uses [GitHub issues] to track the status of the project. 33 | 34 | ### Feature Requests 35 | 36 | Please file a [feature request] if you have an idea for something you'd like to 37 | see implemented in Ocean. 38 | 39 | We have high expectations for what we want to see Ocean become. You are more 40 | than welcome to set the bar even higher! 41 | 42 | ### Bug Reports 43 | 44 | Please file a [bug report] if you encounter something odd or unexpected. 45 | 46 | If you have the chance, before reporting a bug, please [search existing issues], 47 | as it's possible that someone else has already reported your error. This doesn't 48 | always work, and sometimes it's hard to know what to search for, so consider 49 | this extra credit. We won't mind if you accidentally file a duplicate report. 50 | 51 | If you're not sure if something is a bug or not, feel free to file a bug anyway. 52 | 53 | ## Contributing in Pull Requests 54 | 55 | Pull Requests are the main way to directly change the code, documentation, and 56 | dependencies of Ocean. 57 | 58 | Even tiny pull requests (e.g., one character pull request fixing a 59 | tpyo in API documentation) are greatly appreciated. Before making a large 60 | change, it is usually a good idea to first open an issue describing the change 61 | to solicit feedback and guidance. This will increase the likelihood of the PR 62 | getting merged. 63 | 64 | ## License and Copyright 65 | 66 | All code contributions to this project are licensed under the 67 | [GNU Affero General Public License v3](agpl-3.0). 68 | See [`LICENSE.txt`] for full text. 69 | 70 | Other works such as images, videos, and documents are licensed under the 71 | [Creative Commons Attribution-ShareAlike 4.0 International License][by-sa-4.0]. 72 | 73 | By choosing to contribute to this project, copyright over your contributed works 74 | are assigned to Nikolai Vazquez. 75 | 76 | [`LICENSE.txt`]: https://github.com/oceanpkg/ocean/blob/master/LICENSE.txt 77 | [Ocean Code of Conduct]: https://github.com/oceanpkg/ocean/blob/master/CODE_OF_CONDUCT.md 78 | [GitHub issues]: https://github.com/oceanpkg/ocean/issues 79 | [filing an issue]: https://github.com/oceanpkg/ocean/issues/new 80 | [search existing issues]: https://github.com/oceanpkg/ocean/search?q=&type=Issues&utf8=✓ 81 | 82 | [agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html 83 | [by-sa-4.0]: http://creativecommons.org/licenses/by-sa/4.0/ 84 | 85 | 86 | [bug report]: https://github.com/oceanpkg/ocean/issues/new?assignees=nvzqz&labels=kind%2Fbug&template=bug_report.md&title=%5BDescribe+the+problem+you+encountered%5D 87 | [feature request]: https://github.com/oceanpkg/ocean/issues/new?labels=kind%2Ffeature&template=reature_request.md&title=%5BDescribe+the+feature+you%27re+proposing%5D 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Ocean Code of Conduct 2 | 3 | The Ocean community strongly values providing a friendly, safe and welcoming 4 | environment for all, without prejudice. The Ocean team is committed to using 5 | this code of conduct to foster these community values, both online and offline. 6 | 7 | ## Summary 8 | 9 | Harassment in code and discussion or violation of physical boundaries is 10 | completely unacceptable anywhere in Ocean's codebases, issue trackers, Slack, 11 | meetups, and other events. This applies to both private and public settings. 12 | 13 | We will exclude you from interaction if you insult, demean or harass anyone. 14 | That is not welcome behavior. 15 | 16 | ## Our Values 17 | 18 | - Whether you're a regular member or a newcomer, we value making this community 19 | a safe place for you. 20 | 21 | We are committed to providing a friendly, safe and welcoming environment for 22 | all, regardless of level of experience, gender identity and expression, sexual 23 | orientation, disability, personal appearance, body size, race, ethnicity, age, 24 | religion, nationality, or other similar characteristic. 25 | 26 | - Respecting that people may differ in opinion from you leads to healthy 27 | discussion. This may help lead to fruitful understanding of why a certain 28 | approach was taken. Every design or implementation choice carries a 29 | trade-off and numerous costs. There is seldom a right answer. 30 | 31 | ## Conduct 32 | 33 | - Do not **insult**, **demean** or **harass** _anyone_. 34 | 35 | We interpret the term "harassment" as including the definition in the [Citizen 36 | Code of Conduct]; if you have any lack of clarity about what might be included 37 | in that concept, please read their definition. In particular, we don’t 38 | tolerate behavior that excludes people in socially marginalized groups. 39 | 40 | Harassment in a private setting is just as unacceptable. No matter who you 41 | are, if you feel you have been or are being harassed or made uncomfortable by 42 | a community member, please contact one of the core maintainers. 43 | 44 | - Please avoid using overtly sexual aliases or other nicknames that might 45 | detract from a friendly, safe and welcoming environment for all. 46 | 47 | - Please be kind and courteous. There’s no need to be mean or rude. 48 | 49 | - Exercise consideration and respect in your speech and actions. 50 | 51 | - Please keep unstructured critique to a minimum. 52 | 53 | Rather than using language that criticizes the person, use language that 54 | focuses on the code or action itself. 55 | 56 | If you have solid ideas you want to experiment with, make a fork and see how 57 | it works. 58 | 59 | - Likewise any spamming, trolling, flaming, baiting or other attention-stealing 60 | behavior is not welcome. 61 | 62 | ## License and Attribution 63 | 64 |

65 | The Ocean Team has made the 66 | Ocean Code of Conduct available under the 67 | Attribution-ShareAlike 4.0 International 68 | license. This work is published from the 69 | United States. 70 |
71 |
72 | 73 | CC BY-SA 4.0 74 | 75 |

76 | 77 | Portions of this text are derived from or inspired by: 78 | 79 | - [Citizen Code of Conduct] 80 | - [Rust Code of Conduct] 81 | 82 | [Attribution-ShareAlike 4.0 International]: https://creativecommons.org/licenses/by-sa/4.0/ 83 | [Rust Code of Conduct]: https://www.rust-lang.org/policies/code-of-conduct 84 | [Citizen Code of Conduct]: http://citizencodeofconduct.org/ 85 | -------------------------------------------------------------------------------- /cli/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd; 2 | use oceanpkg::Config; 3 | use std::ffi::OsStr; 4 | 5 | mod exec; 6 | 7 | pub fn main(config: &mut Config) -> crate::Result { 8 | let args = cli().get_matches_safe()?; 9 | let args = resolve_aliases(config, args)?; 10 | 11 | match args.subcommand() { 12 | (subcommand, Some(args)) => { 13 | if let Some(run) = cmd::builtin_run_fn(subcommand) { 14 | run(config, args) 15 | } else { 16 | exec::run_external(config, subcommand, args) 17 | } 18 | } 19 | _ => Err(failure::err_msg("No subcommand provided")), 20 | } 21 | } 22 | 23 | fn cli() -> clap::App<'static, 'static> { 24 | clap::App::new("ocean") 25 | .version(env!("CARGO_PKG_VERSION")) 26 | .author(env!("CARGO_PKG_AUTHORS")) 27 | .about(crate::ABOUT) 28 | .settings(&[clap::AppSettings::SubcommandRequiredElseHelp]) 29 | .global_settings(&[ 30 | clap::AppSettings::ColoredHelp, 31 | clap::AppSettings::VersionlessSubcommands, 32 | clap::AppSettings::DeriveDisplayOrder, 33 | clap::AppSettings::AllowExternalSubcommands, 34 | ]) 35 | .set_term_width(80) 36 | .subcommands(cmd::all()) 37 | .arg( 38 | clap::Arg::with_name("verbose") 39 | .short("v") 40 | .long("verbose") 41 | .help("Outputs more debugging information") 42 | .global(true), 43 | ) 44 | } 45 | 46 | fn alias_cli() -> clap::App<'static, 'static> { 47 | cli() 48 | // We're not passing in the binary name, so skip parsing it. 49 | .setting(clap::AppSettings::NoBinaryName) 50 | // The `NoBinaryName` setting removes the name from errors. 51 | .bin_name("ocean") 52 | } 53 | 54 | /// Expands any built-in or user-defined aliases used in `args`. 55 | fn resolve_aliases<'a>( 56 | config: &mut Config, 57 | mut args: clap::ArgMatches<'a>, 58 | ) -> crate::Result> { 59 | // Detect cycles by checking if we've seen the alias before. A pointer to 60 | // the alias start should work since we can get the start address from the 61 | // first reference returned by `UserConfig::parse_alias_os`. 62 | let mut seen_user_aliases = Vec::<*const u8>::new(); 63 | 64 | while let (cmd, Some(sub_args)) = args.subcommand() { 65 | if cmd::builtin_run_fn(cmd).is_some() { 66 | // Built-in commands override user-defined aliases. 67 | 68 | if config.user.aliases.contains_key(cmd) { 69 | eprintln!( 70 | "Ignoring user-defined alias `{}` because it is shadowed \ 71 | by a built-in command", 72 | cmd 73 | ); 74 | } 75 | 76 | // No need to continue resolving. 77 | break; 78 | } else if let Some(mut alias) = config.user.parse_alias_os(cmd) { 79 | // User-defined aliases override built-in aliases. 80 | 81 | // The empty string returns all values. 82 | if let Some(values) = sub_args.values_of_os("") { 83 | alias.extend(values); 84 | } 85 | 86 | // Perform alias cycle detection by checking the start address. 87 | if let Some(&start) = alias.first() { 88 | let start = start as *const OsStr as *const u8; 89 | if seen_user_aliases.contains(&start) { 90 | return Err(failure::format_err!( 91 | "User-defined alias `{}` produces an infinite cycle", 92 | cmd 93 | )); 94 | } else { 95 | seen_user_aliases.push(start); 96 | } 97 | } 98 | 99 | args = alias_cli().get_matches_from_safe(alias)?; 100 | 101 | // The user's alias may resolve to another alias, so we need to 102 | // continue looping until all aliases are resolved. 103 | continue; 104 | } else if let Some(alias) = cmd::builtin_alias(cmd) { 105 | let mut alias = vec![OsStr::new(alias)]; 106 | 107 | // The empty string returns all values. 108 | if let Some(values) = sub_args.values_of_os("") { 109 | alias.extend(values); 110 | } 111 | 112 | args = alias_cli().get_matches_from_safe(alias)?; 113 | 114 | // Built-in aliases always resolve directly to a real command, so 115 | // there's no need to continue looping. 116 | break; 117 | } else { 118 | // The command is neither an alias nor a built-in function. 119 | break; 120 | } 121 | } 122 | 123 | Ok(args) 124 | } 125 | -------------------------------------------------------------------------------- /lib/src/drop/package.rs: -------------------------------------------------------------------------------- 1 | //! Packaging and unpackaging drops. 2 | 3 | use crate::drop::Manifest; 4 | use flate2::{Compression, GzBuilder}; 5 | use std::{ 6 | ffi::OsString, 7 | fs::{self, File}, 8 | io::{self, Read, Seek, SeekFrom}, 9 | path::{self, Path, PathBuf}, 10 | }; 11 | 12 | /// A package drop that can be `ship`ped. 13 | #[derive(Debug)] 14 | pub struct Package { 15 | /// Where the package resides. 16 | pub path: Path, 17 | /// The parsed manifest. 18 | pub manifest: Manifest, 19 | /// An open handle to the file for reading/writing. 20 | pub file: File, 21 | } 22 | 23 | impl Package { 24 | /// Packages a drop in the context of `current_dir`. 25 | /// 26 | /// The manifest is expected to be found at 27 | /// - `manifest_path` or 28 | /// - `current_dir/Ocean.toml`, if `manifest_path` is not provided 29 | pub fn create( 30 | current_dir: A, 31 | manifest_path: Option, 32 | output_dir: Option, 33 | ) -> io::Result 34 | where 35 | A: AsRef, 36 | B: AsRef, 37 | C: AsRef, 38 | { 39 | package_impl( 40 | current_dir.as_ref(), 41 | manifest_path.as_ref().map(|p| p.as_ref()), 42 | output_dir.as_ref().map(|p| p.as_ref()), 43 | ) 44 | } 45 | } 46 | 47 | type TarBuilder<'a> = tar::Builder>; 48 | 49 | fn append_header( 50 | tar: &mut TarBuilder, 51 | tar_path: &Path, // The relative path within the tar file 52 | file: &mut File, 53 | ) -> io::Result<()> { 54 | let mut header = tar::Header::new_gnu(); 55 | header.set_path(tar_path)?; 56 | header.set_metadata(&file.metadata()?); 57 | header.set_cksum(); 58 | tar.append(&header, file) 59 | } 60 | 61 | fn package_impl( 62 | current_dir: &Path, 63 | manifest_path: Option<&Path>, 64 | output_dir: Option<&Path>, 65 | ) -> io::Result { 66 | let manifest_path_buf: PathBuf; 67 | let manifest_path = match manifest_path { 68 | Some(path) => path, 69 | None => { 70 | manifest_path_buf = current_dir.join(Manifest::FILE_NAME); 71 | &manifest_path_buf 72 | } 73 | }; 74 | 75 | let mut manifest_file = File::open(manifest_path)?; 76 | let manifest = { 77 | let mut buf = String::with_capacity(128); 78 | manifest_file.read_to_string(&mut buf)?; 79 | manifest_file.seek(SeekFrom::Start(0))?; 80 | 81 | Manifest::parse_toml(&buf).map_err(|error| { 82 | io::Error::new(io::ErrorKind::InvalidData, error) 83 | })? 84 | }; 85 | 86 | let drop_name = &manifest.meta.name; 87 | let drop_version = &manifest.meta.version; 88 | 89 | let tar_name = format!("{}.tar.gz", drop_name); 90 | let tmp_name = format!(".{}", tar_name); 91 | 92 | let output_dir = output_dir.unwrap_or(current_dir); 93 | let tar_path = output_dir.join(&tar_name); 94 | let tmp_path = output_dir.join(&tmp_name); 95 | 96 | // TODO: Change to `trace!` 97 | println!("Packaging \"{}\"", tar_path.display()); 98 | 99 | fs::DirBuilder::new().recursive(true).create(&output_dir)?; 100 | 101 | let mut tmp_archive = fs::OpenOptions::new() 102 | .read(true) 103 | .write(true) 104 | .create(true) 105 | .open(&tmp_path)?; 106 | 107 | let gz = GzBuilder::new() 108 | .filename(tar_name) 109 | .write(&mut tmp_archive, Compression::best()); 110 | 111 | let mut tar = tar::Builder::new(gz); 112 | let tar_dir = OsString::from(format!( 113 | "{}@{}{}", 114 | drop_name, 115 | drop_version, 116 | path::MAIN_SEPARATOR 117 | )); 118 | 119 | for relative_path in manifest.files() { 120 | let full_path = current_dir.join(&relative_path); 121 | 122 | let mut tar_path = tar_dir.clone(); 123 | tar_path.push(&relative_path); 124 | 125 | append_header( 126 | &mut tar, 127 | tar_path.as_ref(), 128 | &mut File::open(&full_path)?, 129 | )?; 130 | } 131 | 132 | // Append manifest after iterating over its files to reuse `tar_dir`. 133 | { 134 | let mut manifest_tar_path = tar_dir; 135 | manifest_tar_path.push(Manifest::FILE_NAME); 136 | append_header( 137 | &mut tar, 138 | manifest_tar_path.as_ref(), 139 | &mut manifest_file, 140 | )?; 141 | } 142 | 143 | let gz = tar.into_inner()?; 144 | gz.finish()?; 145 | 146 | fs::rename(&tmp_path, &tar_path)?; 147 | 148 | // Set the internal cursor to 0 to allow for subsequent reading. 149 | tmp_archive.seek(SeekFrom::Start(0))?; 150 | 151 | Ok(Package { 152 | path: tar_path, 153 | manifest, 154 | file: tmp_archive, 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /lib/src/api/v1/ship.rs: -------------------------------------------------------------------------------- 1 | use crate::{api, drop::Package}; 2 | use reqwest::{ 3 | header, 4 | multipart::{Form, Part}, 5 | Client, RequestBuilder, 6 | }; 7 | use std::{io, mem}; 8 | 9 | /// Requests an API login token from [`url`]. 10 | /// 11 | /// This appends the `/v1/login` endpoint to the URL. 12 | /// 13 | /// [`url`]: fn.url.html 14 | pub fn ship( 15 | package: &Package, 16 | token: &str, 17 | ) -> Result { 18 | let url = api::url()?; 19 | ship_at(&url, package, token) 20 | } 21 | 22 | /// Requests an API login token from a base API URL. 23 | /// 24 | /// This mainly exists so that we can also issue requests to testing and staging 25 | /// environments. 26 | pub fn ship_at( 27 | api_url: &url::Url, 28 | package: &Package, 29 | token: &str, 30 | ) -> Result { 31 | let url = api_url.join("/v1/packages/create")?; 32 | ship_at_specific(url.as_str(), package, token) 33 | } 34 | 35 | /// Requests an API login token from a specific URL. 36 | pub fn ship_at_specific( 37 | url: U, 38 | package: &Package, 39 | token: &str, 40 | ) -> Result { 41 | // Monomorphized body to slightly reduce the instruction count of the 42 | // binary. 43 | fn ship( 44 | builder: RequestBuilder, 45 | package: &Package, 46 | token: &str, 47 | ) -> Result { 48 | let version = package.manifest.meta.version.to_string(); 49 | 50 | // SAFETY: `Form::text` requires a `'static` lifetime for string slices. 51 | // This lifetime extension is fine to satisfy this requirement because 52 | // the reference does not escape this scope. 53 | let name = unsafe { 54 | let name = package.manifest.meta.name.as_str(); 55 | mem::transmute::<&str, &'static str>(name) 56 | }; 57 | 58 | let form = Form::new().text("name", name).text("version", version); 59 | 60 | // TODO: Replace with `Part::reader` when we figure out how to make that 61 | // work correctly. 62 | let package = Part::file(&package.path)?; 63 | let form = form.part("packageFile", package); 64 | 65 | let response = builder 66 | .multipart(form) 67 | .header(header::COOKIE, format!("token={}", token)) 68 | .send()?; 69 | 70 | // TODO: Change to `debug!` 71 | eprintln!("Received response: {:#?}", response); 72 | 73 | let status = response.status(); 74 | if !status.is_success() { 75 | return Err(ShipError::from(status)); 76 | } 77 | 78 | Ok(response) 79 | } 80 | 81 | ship(Client::new().post(url), package, token) 82 | } 83 | 84 | /// An error returned when attempting to ship a package with Ocean's API. 85 | #[derive(Debug)] 86 | pub enum ShipError { 87 | /// Failed to parse a `Url`. 88 | ParseUrl(url::ParseError), 89 | /// Failed to read the manifest file. 90 | Io(io::Error), 91 | /// Failed to serialize the manifest as JSON. 92 | SerializeManifest(serde_json::Error), 93 | /// Failed to send the request via `reqwest`. 94 | Request(reqwest::Error), 95 | /// Received an error status code. 96 | Status(http::StatusCode), 97 | /// Failed to authenticate (401 status). 98 | Unauthorized, 99 | } 100 | 101 | impl From for ShipError { 102 | fn from(error: url::ParseError) -> Self { 103 | Self::ParseUrl(error) 104 | } 105 | } 106 | 107 | impl From for ShipError { 108 | fn from(error: io::Error) -> Self { 109 | Self::Io(error) 110 | } 111 | } 112 | 113 | impl From for ShipError { 114 | fn from(error: serde_json::Error) -> Self { 115 | Self::SerializeManifest(error) 116 | } 117 | } 118 | 119 | impl From for ShipError { 120 | fn from(error: reqwest::Error) -> Self { 121 | Self::Request(error) 122 | } 123 | } 124 | 125 | impl From for ShipError { 126 | fn from(error: http::StatusCode) -> Self { 127 | if error == http::StatusCode::UNAUTHORIZED { 128 | Self::Unauthorized 129 | } else { 130 | Self::Status(error) 131 | } 132 | } 133 | } 134 | 135 | impl std::fmt::Display for ShipError { 136 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 137 | use self::ShipError::*; 138 | match self { 139 | ParseUrl(error) => error.fmt(f), 140 | Io(error) => error.fmt(f), 141 | SerializeManifest(error) => error.fmt(f), 142 | Request(error) => error.fmt(f), 143 | Status(code) => write!(f, "received response \"{}\"", code), 144 | Unauthorized => write!(f, "incorrect username or password"), 145 | } 146 | } 147 | } 148 | 149 | impl std::error::Error for ShipError {} 150 | -------------------------------------------------------------------------------- /lib/src/drop/source/git.rs: -------------------------------------------------------------------------------- 1 | //! Git repository information. 2 | 3 | use serde::{Serialize, Serializer}; 4 | use std::fmt; 5 | 6 | /// Ocean's git repository. 7 | pub const OCEAN_REPO: &str = env!("CARGO_PKG_REPOSITORY"); 8 | 9 | flexible! { 10 | /// Information about a git repository where a drop or dependency can be found. 11 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)] 12 | pub struct Git { 13 | /// Where the git repository is located. 14 | #[serde(alias = "repository")] 15 | pub repo: String, 16 | /// The specific branch to use. 17 | #[serde(flatten)] 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub reference: Option, 20 | } 21 | } 22 | 23 | impl From for Git { 24 | #[inline] 25 | fn from(repo: String) -> Self { 26 | Self { 27 | repo, 28 | reference: None, 29 | } 30 | } 31 | } 32 | 33 | impl Git { 34 | /// Creates a new instance with the given fields. 35 | pub fn new(repo: A, reference: B) -> Self 36 | where 37 | A: Into, 38 | B: Into>, 39 | { 40 | Self { 41 | repo: repo.into(), 42 | reference: reference.into(), 43 | } 44 | } 45 | 46 | /// Writes the TOML form of `self` to `f`. 47 | #[inline] 48 | pub fn write_toml(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 | write!(f, r#"git = {{ repo = "{}""#, self.repo)?; 50 | if let Some(reference) = &self.reference { 51 | write!(f, r#", {} = "{}""#, reference.kind(), reference)?; 52 | } 53 | write!(f, " }}") 54 | } 55 | 56 | /// Returns a type that can be used to as `{}` to display TOML. 57 | #[inline] 58 | pub fn display_toml<'a>(&'a self) -> impl fmt::Display + Copy + 'a { 59 | #[derive(Clone, Copy)] 60 | struct Displayer<'a>(&'a Git); 61 | 62 | impl fmt::Display for Displayer<'_> { 63 | #[inline] 64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 65 | self.0.write_toml(f) 66 | } 67 | } 68 | 69 | Displayer(self) 70 | } 71 | } 72 | 73 | /// A reference to a git branch/tag/revision. 74 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] 75 | #[serde(rename_all = "lowercase")] 76 | pub enum Ref { 77 | // When adding a case, make sure to add it to `Ref::all`. 78 | /// The specific git branch. 79 | Branch(String), 80 | /// A specific git tag. 81 | Tag(String), 82 | /// A specific git revision. 83 | Rev(String), 84 | } 85 | 86 | impl Default for Ref { 87 | #[inline] 88 | fn default() -> Self { 89 | Self::master() 90 | } 91 | } 92 | 93 | impl fmt::Display for Ref { 94 | #[inline] 95 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 96 | self.as_str().fmt(f) 97 | } 98 | } 99 | 100 | impl AsRef for Ref { 101 | #[inline] 102 | fn as_ref(&self) -> &str { 103 | self.as_str() 104 | } 105 | } 106 | 107 | impl Serialize for Ref { 108 | fn serialize(&self, ser: S) -> Result 109 | where 110 | S: Serializer, 111 | { 112 | use serde::ser::SerializeMap; 113 | 114 | let mut map = ser.serialize_map(Some(1))?; 115 | map.serialize_entry(self.kind(), self.as_str())?; 116 | map.end() 117 | } 118 | } 119 | 120 | impl Ref { 121 | /// Returns an array of all `Ref` variants, each pointing to `reference`. 122 | pub fn all(reference: String) -> [Self; 3] { 123 | [ 124 | Ref::Branch(reference.clone()), 125 | Ref::Tag(reference.clone()), 126 | Ref::Rev(reference), 127 | ] 128 | } 129 | 130 | /// Creates a new `Branch` instance pointing to `reference`. 131 | pub fn branch>(reference: R) -> Self { 132 | Ref::Branch(reference.into()) 133 | } 134 | 135 | /// Creates a new `Tag` instance pointing to `reference`. 136 | pub fn tag>(reference: R) -> Self { 137 | Ref::Tag(reference.into()) 138 | } 139 | 140 | /// Creates a new `Rev` instance pointing to `reference`. 141 | pub fn rev>(reference: R) -> Self { 142 | Ref::Rev(reference.into()) 143 | } 144 | 145 | /// A reference to the master branch. 146 | #[inline] 147 | pub fn master() -> Self { 148 | Ref::branch("master") 149 | } 150 | 151 | /// Returns the reference string. 152 | #[inline] 153 | pub fn as_str(&self) -> &str { 154 | match self { 155 | Self::Branch(r) | Self::Tag(r) | Self::Rev(r) => r, 156 | } 157 | } 158 | 159 | /// Returns the name of the reference kind: `branch`, `tag`, or `rev`. 160 | #[inline] 161 | pub fn kind(&self) -> &'static str { 162 | match self { 163 | Self::Branch(_) => "branch", 164 | Self::Tag(_) => "tag", 165 | Self::Rev(_) => "rev", 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/src/drop/manifest/mod.rs: -------------------------------------------------------------------------------- 1 | //! Drop manifest data. 2 | 3 | use serde::Deserialize; 4 | 5 | mod deps; 6 | mod meta; 7 | 8 | #[cfg(test)] 9 | mod tests; 10 | 11 | #[doc(inline)] 12 | pub use self::{ 13 | deps::{DepInfo, Deps}, 14 | meta::Meta, 15 | }; 16 | 17 | /// A drop manifest. 18 | /// 19 | /// Note the lack of `drop::Name` usage throughout this type. This is because 20 | /// name validation is done by the backend in order for clients to be 21 | /// forward-compatible with later backend versions. 22 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 23 | pub struct Manifest { 24 | /// The drop's info. 25 | pub meta: Meta, 26 | 27 | /// The drops that this drop relies on. 28 | #[serde(rename = "dependencies")] 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub deps: Option, 31 | } 32 | 33 | impl Manifest { 34 | /// The name used for manifest files. 35 | pub const FILE_NAME: &'static str = "Ocean.toml"; 36 | 37 | /// Parses a manifest from [TOML](https://en.wikipedia.org/wiki/TOML). 38 | /// 39 | /// ``` 40 | /// use oceanpkg::drop::Manifest; 41 | /// 42 | /// let toml = r#" 43 | /// [meta] 44 | /// name = "ocean" 45 | /// description = "Cross-platform package manager" 46 | /// version = "0.1.0" 47 | /// license = "AGPL-3.0-only" 48 | /// authors = ["Nikolai Vazquez", "Alex Farra", "Nicole Zhao"] 49 | /// readme = "README.md" 50 | /// changelog = "CHANGELOG.md" 51 | /// git = "https://github.com/oceanpkg/ocean" 52 | /// 53 | /// [dependencies] 54 | /// wget = "*" 55 | /// "#; 56 | /// let manifest = Manifest::parse_toml(toml).unwrap(); 57 | /// ``` 58 | #[cfg(feature = "toml")] 59 | pub fn parse_toml(toml: &str) -> Result { 60 | toml::de::from_str(toml) 61 | } 62 | 63 | /// Parses a manifest from a [TOML](https://en.wikipedia.org/wiki/TOML) file 64 | /// at the given path. 65 | #[cfg(feature = "toml")] 66 | pub fn read_toml_file(toml: T) -> Result 67 | where 68 | T: AsRef, 69 | { 70 | use std::io::{Error, ErrorKind, Read}; 71 | 72 | let mut buf = String::with_capacity(128); 73 | std::fs::File::open(toml)?.read_to_string(&mut buf)?; 74 | Self::parse_toml(&buf) 75 | .map_err(|error| Error::new(ErrorKind::InvalidData, error)) 76 | } 77 | 78 | /// Parses a manifest from [JSON](https://en.wikipedia.org/wiki/JSON). 79 | /// 80 | /// ``` 81 | /// use oceanpkg::drop::Manifest; 82 | /// 83 | /// let json = r#"{ 84 | /// "meta": { 85 | /// "name": "ocean", 86 | /// "description": "Cross-platform package manager", 87 | /// "version": "0.1.0", 88 | /// "license": "AGPL-3.0-only", 89 | /// "authors": ["Nikolai Vazquez", "Alex Farra", "Nicole Zhao"], 90 | /// "readme": "README.md", 91 | /// "changelog": "CHANGELOG.md", 92 | /// "git": "https://github.com/oceanpkg/ocean" 93 | /// }, 94 | /// "dependencies": { 95 | /// "wget": "*" 96 | /// } 97 | /// }"#; 98 | /// let manifest = Manifest::parse_json(json).unwrap(); 99 | /// ``` 100 | pub fn parse_json(json: &str) -> Result { 101 | json::from_str(json) 102 | } 103 | 104 | /// Parses a manifest from [JSON](https://en.wikipedia.org/wiki/JSON) 105 | /// provided by the reader. 106 | pub fn read_json(json: J) -> Result 107 | where 108 | J: std::io::Read, 109 | { 110 | json::from_reader(json) 111 | } 112 | 113 | /// Parses a manifest from a [JSON](https://en.wikipedia.org/wiki/JSON) file 114 | /// at the given path. 115 | pub fn read_json_file(json: J) -> Result 116 | where 117 | J: AsRef, 118 | { 119 | let reader = std::io::BufReader::new(std::fs::File::open(json)?); 120 | Self::read_json(reader).map_err(Into::into) 121 | } 122 | 123 | /// Returns `self` as a TOML string. 124 | #[cfg(feature = "toml")] 125 | pub fn to_toml(&self, pretty: bool) -> Result { 126 | if pretty { 127 | toml::to_string_pretty(self) 128 | } else { 129 | toml::to_string(self) 130 | } 131 | } 132 | 133 | /// Returns `self` as a JSON string. 134 | pub fn to_json(&self, pretty: bool) -> Result { 135 | if pretty { 136 | json::to_string_pretty(self) 137 | } else { 138 | json::to_string(self) 139 | } 140 | } 141 | 142 | /// Returns the list of files to package. 143 | pub fn files(&self) -> Vec<&str> { 144 | let mut files = Vec::new(); 145 | 146 | let exe_path = self.meta.exe_path(); 147 | files.push(exe_path); 148 | 149 | files 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/src/api/v1/login.rs: -------------------------------------------------------------------------------- 1 | use crate::api; 2 | use std::fmt; 3 | 4 | /// Credentials used for API login. 5 | #[derive(Clone, Copy, Serialize)] 6 | #[non_exhaustive] 7 | pub enum Credentials { 8 | /// Basic username/password authentication. 9 | BasicAuth { 10 | /// The user's account name. 11 | username: S, 12 | /// The user's password. This should _never_ be logged or displayed. 13 | password: S, 14 | }, 15 | } 16 | 17 | impl Credentials { 18 | /// Creates a basic username/password authentication. 19 | pub const fn basic_auth(username: S, password: S) -> Self { 20 | Credentials::BasicAuth { username, password } 21 | } 22 | } 23 | 24 | // Required in order to not accidentally log private or personally-identifiable 25 | // information when logging. 26 | impl fmt::Debug for Credentials { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | match self { 29 | Credentials::BasicAuth { .. } => { 30 | f.debug_struct("BasicAuth") 31 | // Personally identifying information 32 | .field("username", &"[pii]") 33 | .field("password", &"[private]") 34 | .finish() 35 | } 36 | } 37 | } 38 | } 39 | 40 | /// Requests an API login token from [`url`]. 41 | /// 42 | /// This appends the `/v1/login` endpoint to the URL. 43 | /// 44 | /// [`url`]: fn.url.html 45 | pub fn request_login_token( 46 | credentials: &Credentials, 47 | ) -> Result { 48 | let url = api::url()?; 49 | request_login_token_at(&url, credentials) 50 | } 51 | 52 | /// Requests an API login token from a base API URL. 53 | /// 54 | /// This mainly exists so that we can also issue requests to testing and staging 55 | /// environments. 56 | pub fn request_login_token_at( 57 | api_url: &url::Url, 58 | credentials: &Credentials, 59 | ) -> Result { 60 | let url = api_url.join("/v1/login")?; 61 | request_login_token_at_specific(url.as_str(), credentials) 62 | } 63 | 64 | /// Requests an API login token from a specific URL. 65 | pub fn request_login_token_at_specific( 66 | url: U, 67 | credentials: &Credentials, 68 | ) -> Result 69 | where 70 | U: reqwest::IntoUrl + fmt::Debug, 71 | S: fmt::Display, 72 | { 73 | // Monomorphized body to slightly reduce the instruction count of the 74 | // binary. 75 | fn request_token( 76 | builder: reqwest::RequestBuilder, 77 | ) -> Result { 78 | let response = builder.send()?; 79 | 80 | let status = response.status(); 81 | if !status.is_success() { 82 | return Err(LoginError::from(status)); 83 | } 84 | 85 | for cookie in response.cookies() { 86 | if cookie.name() == "token" { 87 | return Ok(cookie.value().to_owned()); 88 | } 89 | } 90 | 91 | Err(LoginError::MissingToken) 92 | } 93 | 94 | let mut builder = reqwest::Client::new().post(url); 95 | 96 | match credentials { 97 | Credentials::BasicAuth { username, password } => { 98 | builder = builder.basic_auth(username, Some(password)); 99 | } 100 | } 101 | 102 | request_token(builder) 103 | } 104 | 105 | /// An error returned when attempting to log into Ocean's API. 106 | #[derive(Debug)] 107 | pub enum LoginError { 108 | /// Failed to parse a `Url`. 109 | ParseUrl(url::ParseError), 110 | /// Failed to send the request via `reqwest`. 111 | Request(reqwest::Error), 112 | /// Received an error status code. 113 | Status(http::StatusCode), 114 | /// Failed to authenticate (401 status). 115 | Unauthorized, 116 | /// Received a successful response, but no token was provided. 117 | MissingToken, 118 | } 119 | 120 | impl From for LoginError { 121 | fn from(error: url::ParseError) -> Self { 122 | Self::ParseUrl(error) 123 | } 124 | } 125 | 126 | impl From for LoginError { 127 | fn from(error: reqwest::Error) -> Self { 128 | Self::Request(error) 129 | } 130 | } 131 | 132 | impl From for LoginError { 133 | fn from(error: http::StatusCode) -> Self { 134 | if error == http::StatusCode::UNAUTHORIZED { 135 | Self::Unauthorized 136 | } else { 137 | Self::Status(error) 138 | } 139 | } 140 | } 141 | 142 | impl std::fmt::Display for LoginError { 143 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 144 | use self::LoginError::*; 145 | match self { 146 | ParseUrl(error) => error.fmt(f), 147 | Request(error) => error.fmt(f), 148 | Status(code) => write!(f, "received response \"{}\"", code), 149 | Unauthorized => write!(f, "incorrect username or password"), 150 | MissingToken => write!(f, "login token was not received"), 151 | } 152 | } 153 | } 154 | 155 | impl std::error::Error for LoginError {} 156 | -------------------------------------------------------------------------------- /lib/src/drop/manifest/tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | 3 | use super::*; 4 | use crate::drop::{ 5 | license::{self, SpdxLicense}, 6 | name::{Name, Query}, 7 | source::git::{self, Git, OCEAN_REPO}, 8 | version::SemVer, 9 | }; 10 | 11 | #[cfg(feature = "toml")] 12 | fn manifests() -> Vec<(String, Manifest)> { 13 | let version = "0.1.0"; 14 | let semver = SemVer::parse(version).unwrap(); 15 | let repo = OCEAN_REPO; 16 | let home = "https://www.oceanpkg.org"; 17 | let docs = "https://docs.oceanpkg.org"; 18 | let wget = Query::<&str>::parse_liberal("wget"); 19 | let meta = Meta { 20 | name: "ocean".to_owned(), 21 | display_name: Some("Ocean".to_owned()), 22 | description: "Cross-platform package manager".to_owned(), 23 | exe_path: None, 24 | version: semver.into(), 25 | conflicts: None, 26 | license: Some(SpdxLicense::Agpl3Only.id().to_owned()), 27 | authors: Some(vec![ 28 | "Nikolai Vazquez".to_owned(), 29 | "Alex Farra".to_owned(), 30 | "Nicole Zhao".to_owned(), 31 | ]), 32 | readme: Some("README.md".to_owned()), 33 | changelog: Some("CHANGELOG.md".to_owned()), 34 | git: Some(Git { 35 | repo: repo.to_owned(), 36 | reference: Some(git::Ref::Tag(version.to_owned())), 37 | }), 38 | homepage: Some(home.to_owned()), 39 | documentation: Some(docs.to_owned()), 40 | }; 41 | let header = format!( 42 | r#" 43 | [meta] 44 | name = "ocean" 45 | display-name = "Ocean" 46 | description = "Cross-platform package manager" 47 | version = "{version}" 48 | license = "AGPL-3.0-only" 49 | authors = ["Nikolai Vazquez", "Alex Farra", "Nicole Zhao"] 50 | readme = "README.md" 51 | changelog = "CHANGELOG.md" 52 | git = {{ repo = "{repo}", tag = "{version}" }} 53 | homepage = "{homepage}" 54 | documentation = "{documentation}" 55 | "#, 56 | version = version, 57 | repo = repo, 58 | homepage = home, 59 | documentation = docs, 60 | ); 61 | let detailed_deps: Deps = vec![( 62 | wget.to_owned(), 63 | DepInfo { 64 | version: "*".to_owned(), 65 | optional: false, 66 | git: Some(Git::new( 67 | "https://git.savannah.gnu.org/git/wget.git", 68 | git::Ref::branch("1.0"), 69 | )), 70 | }, 71 | )] 72 | .into_iter() 73 | .collect(); 74 | vec![ 75 | ( 76 | format!( 77 | r#" 78 | {} 79 | [dependencies] 80 | wget = "*" 81 | "#, 82 | header, 83 | ), 84 | Manifest { 85 | meta: meta.clone(), 86 | deps: Some( 87 | vec![(wget.to_owned(), "*".to_owned().into())] 88 | .into_iter() 89 | .collect(), 90 | ), 91 | }, 92 | ), 93 | ( 94 | format!( 95 | r#" 96 | {} 97 | [dependencies] 98 | wget = {{ version = "*", git = {{ repo = "https://git.savannah.gnu.org/git/wget.git", branch = "1.0" }} }} 99 | "#, 100 | header, 101 | ), 102 | Manifest { 103 | meta: meta.clone(), 104 | deps: Some(detailed_deps.clone()), 105 | }, 106 | ), 107 | ( 108 | format!( 109 | r#" 110 | {} 111 | [dependencies.wget] 112 | version = "*" 113 | git = {{ repo = "https://git.savannah.gnu.org/git/wget.git", branch = "1.0" }} 114 | "#, 115 | header, 116 | ), 117 | Manifest { 118 | meta, 119 | deps: Some(detailed_deps), 120 | }, 121 | ), 122 | ] 123 | } 124 | 125 | fn example_manifest() -> Manifest { 126 | Manifest { 127 | meta: Meta { 128 | name: "wumbo".to_owned(), 129 | display_name: Some("Wumbo".to_owned()), 130 | description: "Something silly".to_owned(), 131 | exe_path: Some("wumbo".to_owned()), 132 | version: SemVer::new(0, 1, 0).into(), 133 | conflicts: None, 134 | license: Some("MIT OR AGPL-3.0-only".to_owned()), 135 | authors: Some(vec![ 136 | "Nikolai Vazquez".to_owned(), 137 | "Patrick Star".to_owned(), 138 | ]), 139 | readme: Some("../README.md".to_owned()), 140 | changelog: Some("../CHANGELOG.md".to_owned()), 141 | git: Some(Git::new(OCEAN_REPO, git::Ref::tag("v0.1.0"))), 142 | homepage: Some("https://example.com".to_owned()), 143 | documentation: Some("https://example.com/docs".to_owned()), 144 | }, 145 | deps: Some(vec![].into_iter().collect()), 146 | } 147 | } 148 | 149 | #[cfg(feature = "toml")] 150 | mod toml { 151 | use super::*; 152 | 153 | #[test] 154 | fn deserialize_manfiest() { 155 | for (toml, manifest) in manifests() { 156 | let parsed = Manifest::parse_toml(&toml).unwrap(); 157 | assert_eq!(manifest, parsed, "\n{:#?}\n{:#?}\n", manifest, parsed); 158 | } 159 | } 160 | 161 | #[test] 162 | fn serialize_manifest() { 163 | let manifest = example_manifest(); 164 | manifest.to_toml(false).unwrap(); 165 | manifest.to_toml(true).unwrap(); 166 | } 167 | } 168 | 169 | mod json { 170 | use super::*; 171 | 172 | #[test] 173 | fn serialize_manifest() { 174 | let manifest = example_manifest(); 175 | manifest.to_json(false).unwrap(); 176 | manifest.to_json(true).unwrap(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/src/flexible.rs: -------------------------------------------------------------------------------- 1 | /// Defines a `struct` or `enum` such that it can flexibly be parsed via 2 | /// `serde::Deserialize` as either: 3 | /// - Only the first field/variant type 4 | /// - Key/value pairs 5 | macro_rules! flexible { 6 | // Parse struct 7 | ( 8 | $(#[serde($($t_serde:meta),+ $(,)?)])* 9 | $(#[$t_meta:meta])* 10 | $t_vis:vis struct $t:ident $(<$l:lifetime>)? { 11 | // The field used for the `Simple` form. 12 | $(#[doc = $s_doc:literal])+ 13 | $(#[serde($($s_serde:meta),+ $(,)?)])* 14 | $s_vis:vis $s:ident: $s_ty:ty, 15 | 16 | $( 17 | $(#[doc = $f_doc:literal])+ 18 | $(#[serde($($f_serde:meta),+ $(,)?)])* 19 | $f_vis:vis $f:ident: $f_ty:ty, 20 | )+ 21 | } 22 | ) => { 23 | // The actual struct definition. 24 | $(#[$t_meta])* 25 | $t_vis struct $t $(<$l>)? { 26 | $(#[doc = $s_doc])+ 27 | $s_vis $s: $s_ty, 28 | 29 | $( 30 | $(#[doc = $f_doc])+ 31 | $f_vis $f: $f_ty, 32 | )* 33 | } 34 | 35 | impl<'de $(: $l)? $(, $l)?> serde::Deserialize<'de> for $t $(<$l>)? { 36 | #[inline] 37 | fn deserialize(de: D) -> Result 38 | where 39 | D: serde::Deserializer<'de> 40 | { 41 | use crate::flexible::{Flexible, Detailed}; 42 | 43 | // Wrapper type is in a module so that it can have the same 44 | // name as the original. 45 | mod wrapper { 46 | #[allow(unused_imports)] 47 | use super::*; 48 | 49 | $(#[serde($($t_serde),+)])* 50 | #[derive(Deserialize)] 51 | pub struct $t $(<$l>)? { 52 | $(#[serde($($s_serde),+)])* 53 | #[allow(unused)] 54 | $s: $s_ty, 55 | 56 | $( 57 | $(#[serde($($f_serde),+)])* 58 | #[allow(unused)] 59 | $f: $f_ty, 60 | )* 61 | } 62 | } 63 | 64 | impl $(<$l>)? Detailed for wrapper::$t $(<$l>)? { 65 | type Simple = $s_ty; 66 | } 67 | 68 | impl $(<$l>)? Detailed for $t $(<$l>)? { 69 | type Simple = $s_ty; 70 | } 71 | 72 | Flexible::::deserialize(de) 73 | .map(|wrapper| unsafe { 74 | std::mem::transmute::<_, Flexible<$t>>(wrapper) 75 | }) 76 | .map(Flexible::into_detailed) 77 | } 78 | } 79 | }; 80 | // Parse enum 81 | ( 82 | $(#[serde($($t_serde:meta),+ $(,)?)])* 83 | $(#[$t_meta:meta])* 84 | $t_vis:vis enum $t:ident $(<$l:lifetime>)? { 85 | // The variant used for the `Simple` form. 86 | $(#[doc = $s_doc:literal])+ 87 | $(#[serde($($s_serde:meta),+ $(,)?)])* 88 | $s:ident ($s_ty:ty), 89 | 90 | $( 91 | $(#[doc = $v_doc:literal])+ 92 | $(#[serde($($v_serde:meta),+ $(,)?)])* 93 | $v:ident ($v_ty:ty), 94 | )+ 95 | } 96 | ) => { 97 | // The actual enum definition. 98 | $(#[$t_meta])* 99 | $t_vis enum $t $(<$l>)? { 100 | $(#[doc = $s_doc])+ 101 | $s($s_ty), 102 | 103 | $( 104 | $(#[doc = $v_doc])+ 105 | $v($v_ty), 106 | )* 107 | } 108 | 109 | impl<'de $(: $l)? $(, $l)?> serde::Deserialize<'de> for $t $(<$l>)? { 110 | #[inline] 111 | fn deserialize(de: D) -> Result 112 | where 113 | D: serde::Deserializer<'de> 114 | { 115 | use crate::flexible::{Flexible, Detailed}; 116 | 117 | // Wrapper type is in a module so that it can have the same 118 | // name as the original. 119 | mod wrapper { 120 | #[allow(unused_imports)] 121 | use super::*; 122 | 123 | $(#[serde($($t_serde),+)])* 124 | #[derive(Deserialize)] 125 | $t_vis enum $t $(<$l>)? { 126 | $(#[serde($($s_serde),+)])* 127 | #[allow(unused)] 128 | $s($s_ty), 129 | 130 | $( 131 | $(#[serde($($v_serde),+)])* 132 | #[allow(unused)] 133 | $v($v_ty), 134 | )* 135 | } 136 | } 137 | 138 | impl $(<$l>)? Detailed for wrapper::$t $(<$l>)? { 139 | type Simple = $s_ty; 140 | } 141 | 142 | impl $(<$l>)? Detailed for $t $(<$l>)? { 143 | type Simple = $s_ty; 144 | } 145 | 146 | Flexible::::deserialize(de) 147 | .map(|wrapper| unsafe { 148 | std::mem::transmute::<_, Flexible<$t>>(wrapper) 149 | }) 150 | .map(Flexible::into_detailed) 151 | } 152 | } 153 | }; 154 | } 155 | 156 | /// A type that has detailed information. 157 | pub(crate) trait Detailed: Sized { 158 | /// The basic version of this type. 159 | type Simple; 160 | } 161 | 162 | /// A type that can either be parsed as simple or detailed information. 163 | #[derive(Deserialize)] 164 | #[serde(untagged)] 165 | pub(crate) enum Flexible { 166 | /// The minimal amount of information that is within `D`. 167 | Simple(D::Simple), 168 | /// All information stored within `D`. 169 | Detailed(D), 170 | } 171 | 172 | impl Flexible { 173 | /// Converts `self` into the detailed form `D` so that all information can 174 | /// be used in a simple way without extra `match`ing. 175 | #[inline] 176 | pub fn into_detailed(self) -> D 177 | where 178 | D::Simple: Into, 179 | { 180 | match self { 181 | Self::Simple(s) => s.into(), 182 | Self::Detailed(d) => d, 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/src/drop/name/mod.rs: -------------------------------------------------------------------------------- 1 | //! Drop names. 2 | 3 | use std::{convert::TryInto, fmt}; 4 | 5 | mod parse; 6 | pub mod query; 7 | // pub mod query2; 8 | pub mod scoped; 9 | 10 | #[doc(inline)] 11 | pub use self::{query::Query, scoped::ScopedName}; 12 | 13 | /// A valid drop name. 14 | /// 15 | /// Valid names are non-empty, lowercase ASCII alphanumeric, and can have dashes 16 | /// (`-`) anywhere except for the beginning or end. 17 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 18 | pub struct Name(str); 19 | 20 | impl From<&Name> for Box { 21 | #[inline] 22 | fn from(name: &Name) -> Self { 23 | name.to_boxed() 24 | } 25 | } 26 | 27 | impl Clone for Box { 28 | #[inline] 29 | #[allow(clippy::borrowed_box)] 30 | fn clone(&self) -> Self { 31 | let str_box: &Box = 32 | unsafe { &*(self as *const Self as *const Box) }; 33 | let str_box = str_box.clone(); 34 | let raw = Box::into_raw(str_box) as *mut Name; 35 | unsafe { Box::from_raw(raw) } 36 | } 37 | } 38 | 39 | // Allows for creating a `&Name` in a `const` from a `&str`. 40 | macro_rules! valid_name { 41 | ($name:expr) => {{ 42 | union Convert<'a> { 43 | s: &'a str, 44 | n: &'a Name, 45 | } 46 | Convert { s: $name }.n 47 | }}; 48 | } 49 | 50 | impl AsRef for Name { 51 | #[inline] 52 | fn as_ref(&self) -> &str { 53 | self.as_str() 54 | } 55 | } 56 | 57 | impl AsRef<[u8]> for Name { 58 | #[inline] 59 | fn as_ref(&self) -> &[u8] { 60 | self.0.as_bytes() 61 | } 62 | } 63 | 64 | impl PartialEq for Name { 65 | #[inline] 66 | fn eq(&self, s: &str) -> bool { 67 | self.0 == *s 68 | } 69 | } 70 | 71 | impl PartialEq<[u8]> for Name { 72 | #[inline] 73 | fn eq(&self, b: &[u8]) -> bool { 74 | self.0.as_bytes() == b 75 | } 76 | } 77 | 78 | impl PartialEq for str { 79 | #[inline] 80 | fn eq(&self, n: &Name) -> bool { 81 | *self == n.0 82 | } 83 | } 84 | 85 | impl PartialEq for [u8] { 86 | #[inline] 87 | fn eq(&self, n: &Name) -> bool { 88 | self == n.0.as_bytes() 89 | } 90 | } 91 | 92 | impl fmt::Display for Name { 93 | #[inline] 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | self.0.fmt(f) 96 | } 97 | } 98 | 99 | impl Name { 100 | /// The string "core". 101 | pub const CORE: &'static Self = unsafe { valid_name!("core") }; 102 | 103 | /// The string "ocean". 104 | pub const OCEAN: &'static Self = unsafe { valid_name!("ocean") }; 105 | 106 | /// The string "self". 107 | pub const SELF: &'static Self = unsafe { valid_name!("self") }; 108 | 109 | /// Namespaces reserved to only be used only by Ocean. 110 | pub const RESERVED_SCOPES: &'static [&'static Self] = 111 | &[Self::CORE, Self::OCEAN, Self::SELF]; 112 | 113 | /// Attempts to create a new instance by parsing `name`. 114 | #[inline] 115 | pub fn new<'a, N>(name: N) -> Result<&'a Self, ValidateError> 116 | where 117 | N: TryInto<&'a Self, Error = ValidateError>, 118 | { 119 | name.try_into() 120 | } 121 | 122 | /// Creates a new instance without parsing `name`. 123 | #[allow(clippy::missing_safety_doc)] // TODO: Add `# Safety` section 124 | pub unsafe fn new_unchecked(name: &N) -> &Self 125 | where 126 | N: ?Sized + AsRef<[u8]>, 127 | { 128 | &*(name.as_ref() as *const [u8] as *const Self) 129 | } 130 | 131 | /// Returns whether `name` is valid. 132 | /// 133 | /// All characters in `name` must match the regex `[0-9a-z-]`, with the 134 | /// exception of the first and last character where `-` is not allowed. 135 | #[inline] 136 | pub fn is_valid>(name: N) -> bool { 137 | // Monomorphized 138 | fn imp(bytes: &[u8]) -> bool { 139 | match (bytes.first(), bytes.last()) { 140 | // Cannot be empty or begin/end with '-' 141 | (None, _) | (Some(b'-'), _) | (_, Some(b'-')) => false, 142 | _ => bytes.iter().cloned().all(Name::is_valid_ascii), 143 | } 144 | } 145 | imp(name.as_ref()) 146 | } 147 | 148 | /// Returns whether `byte` is valid within a name. 149 | /// 150 | /// Regex: `[0-9a-z-]`. 151 | /// 152 | /// Note that this returns `true` for `-` despite it being invalid at the 153 | /// start and end of a full name. 154 | #[inline] 155 | pub fn is_valid_ascii(byte: u8) -> bool { 156 | match byte { 157 | b'0'..=b'9' | b'a'..=b'z' | b'-' => true, 158 | _ => false, 159 | } 160 | } 161 | 162 | /// Returns whether the unicode scalar is valid within a name. 163 | /// 164 | /// See [`is_valid_ascii`](#method.is_valid_ascii) for more info. 165 | #[inline] 166 | pub fn is_valid_char(ch: char) -> bool { 167 | ch.is_ascii() && Self::is_valid_ascii(ch as u8) 168 | } 169 | 170 | /// Converts `self` to the underlying UTF-8 string slice. 171 | #[inline] 172 | pub const fn as_str(&self) -> &str { 173 | &self.0 174 | } 175 | 176 | /// Moves copied contents of `self` to the heap. 177 | #[inline] 178 | pub fn to_boxed(&self) -> Box { 179 | let raw = Box::::into_raw(self.0.into()) as *mut Name; 180 | unsafe { Box::from_raw(raw) } 181 | } 182 | 183 | /// Returns whether Ocean reserves the right to use this name as a scope. 184 | #[inline] 185 | pub fn is_reserved_scope(&self) -> bool { 186 | Self::RESERVED_SCOPES.contains(&self) 187 | } 188 | } 189 | 190 | /// Error returned when a [`Name`](struct.Name.html) could not be created. 191 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 192 | pub struct ValidateError(pub(super) ()); 193 | 194 | impl fmt::Display for ValidateError { 195 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 196 | write!(f, "failed to validate drop name") 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | 204 | // Legal outer characters 205 | fn outer() -> Vec { 206 | let mut outer = Vec::new(); 207 | outer.extend((b'a'..=b'z').map(|byte| byte as char)); 208 | outer.extend((b'0'..=b'9').map(|byte| byte as char)); 209 | outer 210 | } 211 | 212 | #[test] 213 | fn valid_names() { 214 | let outer = outer(); 215 | let mut inner = outer.clone(); 216 | inner.push('-'); 217 | 218 | for &c1 in &outer { 219 | let mut name_buf = [0; 4]; 220 | let name = c1.encode_utf8(&mut name_buf); 221 | assert!(Name::is_valid(&name), "{:?} found to be invalid", name); 222 | 223 | for &c2 in &inner { 224 | for &c3 in &outer { 225 | let name: String = [c1, c2, c3].iter().collect(); 226 | assert!( 227 | Name::is_valid(&name), 228 | "{:?} found to be invalid", 229 | name 230 | ); 231 | } 232 | } 233 | } 234 | } 235 | 236 | #[test] 237 | fn invalid_names() { 238 | assert!(!Name::is_valid("")); 239 | assert!(!Name::is_valid("-")); 240 | assert!(!Name::is_valid("--")); 241 | assert!(!Name::is_valid("---")); 242 | 243 | for &ch in &outer() { 244 | let names: &[&[char]] = &[&[ch, '-'], &['-', ch], &['-', ch, '-']]; 245 | for name in names { 246 | let name: String = name.iter().cloned().collect(); 247 | assert!( 248 | !Name::is_valid(&name), 249 | "{:?} found to to be valid", 250 | name 251 | ); 252 | } 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /lib/src/drop/name/parse.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | query::{self, Query}, 3 | scoped::{self, ScopedName}, 4 | Name, ValidateError, 5 | }; 6 | use serde::{ 7 | de::{self, Deserialize, Deserializer, Visitor}, 8 | ser::{Serialize, Serializer}, 9 | }; 10 | use shared::ext::OsStrExt; 11 | use std::{ 12 | convert::{TryFrom, TryInto}, 13 | ffi::{CStr, OsStr}, 14 | fmt, 15 | marker::PhantomData, 16 | str, 17 | }; 18 | 19 | impl<'a> TryFrom<&'a str> for &'a Name { 20 | type Error = ValidateError; 21 | 22 | #[inline] 23 | fn try_from(s: &'a str) -> Result { 24 | TryFrom::try_from(s.as_bytes()) 25 | } 26 | } 27 | 28 | impl TryFrom<&str> for Box { 29 | type Error = ValidateError; 30 | 31 | #[inline] 32 | fn try_from(s: &str) -> Result { 33 | <&Name>::try_from(s).map(|name| name.to_boxed()) 34 | } 35 | } 36 | 37 | impl<'a> TryFrom<&'a [u8]> for &'a Name { 38 | type Error = ValidateError; 39 | 40 | fn try_from(bytes: &'a [u8]) -> Result { 41 | if Name::is_valid(bytes) { 42 | Ok(unsafe { &*(bytes as *const [u8] as *const Name) }) 43 | } else { 44 | Err(ValidateError(())) 45 | } 46 | } 47 | } 48 | 49 | impl<'a> TryFrom<&'a CStr> for &'a Name { 50 | type Error = ValidateError; 51 | 52 | #[inline] 53 | fn try_from(s: &'a CStr) -> Result { 54 | Self::try_from(s.to_bytes()) 55 | } 56 | } 57 | 58 | impl<'a> TryFrom<&'a OsStr> for &'a Name { 59 | type Error = ValidateError; 60 | 61 | fn try_from(s: &'a OsStr) -> Result { 62 | s.try_as_bytes() 63 | .ok_or(ValidateError(())) 64 | .and_then(TryFrom::try_from) 65 | } 66 | } 67 | 68 | impl<'de> Deserialize<'de> for Box { 69 | #[inline] 70 | fn deserialize(deserializer: D) -> Result 71 | where 72 | D: Deserializer<'de>, 73 | { 74 | struct Vis; 75 | 76 | impl<'de> Visitor<'de> for Vis { 77 | type Value = Box; 78 | 79 | #[inline] 80 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 81 | f.write_str("a valid drop name") 82 | } 83 | 84 | #[inline] 85 | fn visit_str(self, v: &str) -> Result 86 | where 87 | E: de::Error, 88 | { 89 | Name::new(v).map(Into::into).map_err(E::custom) 90 | } 91 | 92 | #[inline] 93 | fn visit_bytes(self, v: &[u8]) -> Result 94 | where 95 | E: de::Error, 96 | { 97 | Name::new(v).map(Into::into).map_err(E::custom) 98 | } 99 | } 100 | 101 | deserializer.deserialize_str(Vis) 102 | } 103 | } 104 | 105 | impl<'de: 'a, 'a> Deserialize<'de> for &'a Name { 106 | #[inline] 107 | fn deserialize(deserializer: D) -> Result 108 | where 109 | D: Deserializer<'de>, 110 | { 111 | struct Vis; 112 | 113 | impl<'de> Visitor<'de> for Vis { 114 | type Value = &'de Name; 115 | 116 | #[inline] 117 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 118 | f.write_str("a valid drop name") 119 | } 120 | 121 | #[inline] 122 | fn visit_borrowed_str( 123 | self, 124 | v: &'de str, 125 | ) -> Result 126 | where 127 | E: de::Error, 128 | { 129 | Name::new(v).map_err(E::custom) 130 | } 131 | 132 | #[inline] 133 | fn visit_borrowed_bytes( 134 | self, 135 | v: &'de [u8], 136 | ) -> Result 137 | where 138 | E: de::Error, 139 | { 140 | Name::new(v).map_err(E::custom) 141 | } 142 | } 143 | 144 | deserializer.deserialize_str(Vis) 145 | } 146 | } 147 | 148 | impl Serialize for Name { 149 | #[inline] 150 | fn serialize(&self, s: S) -> Result { 151 | s.serialize_str(self.as_str()) 152 | } 153 | } 154 | 155 | //============================================================================== 156 | 157 | // TODO: Reintroduce `TryFrom<&[u8]>`. 158 | impl<'a, N> TryFrom<&'a str> for ScopedName 159 | where 160 | &'a str: TryInto, 161 | { 162 | type Error = scoped::ParseError<<&'a str as TryInto>::Error>; 163 | 164 | fn try_from(name: &'a str) -> Result { 165 | let mut scope_iter = name.splitn(2, '/'); 166 | match (scope_iter.next(), scope_iter.next()) { 167 | (None, _) => unreachable!(), 168 | (_, None) => Err(scoped::ParseError::MissingSeparator), 169 | (Some(scope), Some(name)) => ScopedName { scope, name }.try_cast(), 170 | } 171 | } 172 | } 173 | 174 | impl<'de> Deserialize<'de> for ScopedName { 175 | #[inline] 176 | fn deserialize(deserializer: D) -> Result 177 | where 178 | D: Deserializer<'de>, 179 | { 180 | struct Vis; 181 | 182 | impl<'de> Visitor<'de> for Vis { 183 | type Value = ScopedName; 184 | 185 | #[inline] 186 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 187 | f.write_str("a valid drop name") 188 | } 189 | 190 | #[inline] 191 | fn visit_str(self, v: &str) -> Result 192 | where 193 | E: de::Error, 194 | { 195 | ScopedName::parse(v).map_err(E::custom) 196 | } 197 | } 198 | 199 | deserializer.deserialize_str(Vis) 200 | } 201 | } 202 | 203 | impl Serialize for ScopedName 204 | where 205 | ScopedName: fmt::Display, 206 | { 207 | #[inline] 208 | fn serialize(&self, s: S) -> Result { 209 | s.collect_str(self) 210 | } 211 | } 212 | 213 | //============================================================================== 214 | 215 | // TODO: Reintroduce `TryFrom<&[u8]>`. 216 | impl<'a, N, V> TryFrom<&'a str> for Query 217 | where 218 | &'a str: TryInto, 219 | &'a str: TryInto, 220 | { 221 | type Error = query::ParseError< 222 | <&'a str as TryInto>::Error, 223 | <&'a str as TryInto>::Error, 224 | >; 225 | 226 | #[inline] 227 | fn try_from(query: &'a str) -> Result { 228 | Query::<&str>::parse_liberal(query).try_cast() 229 | } 230 | } 231 | 232 | assert_impl_all!(Query, String>: Deserialize<'static>); 233 | 234 | impl<'de, N, V, NE, VE> Deserialize<'de> for Query 235 | where 236 | for<'a> &'a str: TryInto, Error = query::ParseError>, 237 | query::ParseError: fmt::Display, 238 | { 239 | #[inline] 240 | fn deserialize(deserializer: D) -> Result 241 | where 242 | D: Deserializer<'de>, 243 | { 244 | struct Vis(PhantomData<(N, V)>); 245 | 246 | impl<'de, N, V, NE, VE> Visitor<'de> for Vis 247 | where 248 | for<'a> &'a str: 249 | TryInto, Error = query::ParseError>, 250 | query::ParseError: fmt::Display, 251 | { 252 | type Value = Query; 253 | 254 | #[inline] 255 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 256 | f.write_str("a valid drop name") 257 | } 258 | 259 | #[inline] 260 | fn visit_str(self, v: &str) -> Result 261 | where 262 | E: de::Error, 263 | { 264 | TryInto::::try_into(v).map_err(E::custom) 265 | } 266 | } 267 | 268 | deserializer.deserialize_str(Vis(PhantomData)) 269 | } 270 | } 271 | 272 | impl Serialize for Query 273 | where 274 | Query: fmt::Display, 275 | { 276 | #[inline] 277 | fn serialize(&self, s: S) -> Result { 278 | s.collect_str(self) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /lib/src/config/rt.rs: -------------------------------------------------------------------------------- 1 | //! Runtime configuration data. 2 | 3 | use crate::{drop::name::Query, install::InstallTarget}; 4 | use lazycell::LazyCell; 5 | use std::{ 6 | borrow::Cow, 7 | env, 8 | error::Error, 9 | ffi::{OsStr, OsString}, 10 | fmt, io, 11 | path::{Path, PathBuf}, 12 | time::{Duration, Instant}, 13 | }; 14 | 15 | /// Represents the configuration specific to this current instance. 16 | /// 17 | /// This type exists so as to put creation of commonly-needed values up-front, 18 | /// allowing for errors only needed to be handled in one place. 19 | #[derive(Clone, Debug)] 20 | pub struct RtConfig { 21 | /// The time at which the program started. Used for telling how much time 22 | /// has elapsed. 23 | pub start_time: Instant, 24 | /// The directory where this process was started from. 25 | pub current_dir: PathBuf, 26 | /// The process's executable. 27 | pub current_exe: LazyCell, 28 | /// The current user's home directory. 29 | pub user_home: PathBuf, 30 | /// The directory for data stored for the current user. 31 | /// 32 | /// | Platform | Path | 33 | /// | :------------ | :-------------- | 34 | /// | Linux & macOS | `$HOME/.ocean` | 35 | /// | Windows | _Unimplemented_ | 36 | pub ocean_home: LazyCell, 37 | /// The `PATH` environment variable. 38 | pub path_var: LazyCell, 39 | } 40 | 41 | impl RtConfig { 42 | /// Creates a new instance suitable for using at the start of your program. 43 | #[inline] 44 | pub fn create() -> Result { 45 | Self::create_at(Instant::now()) 46 | } 47 | 48 | /// Creates a new instance suitable for emitting metrics from `start_time`. 49 | pub fn create_at(start_time: Instant) -> Result { 50 | Ok(Self { 51 | start_time, 52 | current_dir: env::current_dir() 53 | .map_err(CreateError::MissingCurrentDir)?, 54 | current_exe: LazyCell::new(), 55 | user_home: dirs::home_dir() 56 | .ok_or_else(|| CreateError::MissingUserHome)?, 57 | ocean_home: LazyCell::new(), 58 | path_var: LazyCell::new(), 59 | }) 60 | } 61 | 62 | /// Returns the amount of time elapsed since the program started. 63 | #[inline] 64 | pub fn time_elapsed(&self) -> Duration { 65 | self.start_time.elapsed() 66 | } 67 | 68 | /// The directory where this process was started from. 69 | #[inline] 70 | pub fn current_dir(&self) -> &Path { 71 | &self.current_dir 72 | } 73 | 74 | /// The process's executable. 75 | #[inline] 76 | pub fn current_exe(&self) -> Result<&Path, MissingCurrentExe> { 77 | fn from_env() -> io::Result { 78 | env::current_exe()?.canonicalize() 79 | } 80 | 81 | fn from_argv(config: &RtConfig) -> Result { 82 | let argv0 = env::args_os() 83 | .map(PathBuf::from) 84 | .next() 85 | .ok_or(MissingCurrentExe(()))?; 86 | 87 | crate::path::resolve_exe(&argv0, config.path_var().ok()) 88 | .map_err(|_| MissingCurrentExe(())) 89 | } 90 | 91 | self.current_exe 92 | .try_borrow_with(|| { 93 | if let Ok(exe) = from_env() { 94 | Ok(exe) 95 | } else { 96 | from_argv(self) 97 | } 98 | }) 99 | .map(AsRef::as_ref) 100 | } 101 | 102 | /// The directory where data for the current user is stored. 103 | pub fn ocean_home(&self) -> &Path { 104 | self.ocean_home 105 | .borrow_with(|| self.user_home().join(".ocean")) 106 | } 107 | 108 | /// The current user's home directory. 109 | #[inline] 110 | pub fn user_home(&self) -> &Path { 111 | &self.user_home 112 | } 113 | 114 | /// The `PATH` environment variable. 115 | #[inline] 116 | pub fn path_var(&self) -> Result<&OsStr, MissingPathVar> { 117 | self.path_var 118 | .try_borrow_with(|| env::var_os("PATH").ok_or(MissingPathVar(()))) 119 | .map(AsRef::as_ref) 120 | } 121 | 122 | /// Returns the path for `$HOME/.ocean/credentials.toml`. 123 | pub fn credentials_path(&self) -> PathBuf { 124 | self.ocean_home().join("credentials.toml") 125 | } 126 | 127 | /// Returns the directory where binaries exposed via `$PATH` are stored. 128 | pub fn bin_dir(&self) -> PathBuf { 129 | #[cfg(unix)] 130 | { 131 | self.ocean_home().join("bin") 132 | } 133 | 134 | #[cfg(windows)] 135 | unimplemented!("TODO: Write & test on Windows :)"); 136 | } 137 | 138 | /// Returns Ocean's cache directory. 139 | pub fn cache_dir(&self) -> PathBuf { 140 | self.ocean_home().join("cache") 141 | } 142 | 143 | /// Returns the path where a tarball for `query` should be cached. 144 | pub fn tarball_cache_path(&self, query: Query<&str>) -> PathBuf { 145 | let mut path = self.cache_dir(); 146 | path.push(query.tarball_name()); 147 | path 148 | } 149 | 150 | /// Returns the directory where drops are installed. 151 | pub fn drops_dir(&self, target: &InstallTarget) -> Cow<'static, Path> { 152 | #[cfg(unix)] 153 | match target { 154 | InstallTarget::CurrentUser => { 155 | Cow::Owned(self.ocean_home().join("drops")) 156 | } 157 | InstallTarget::SpecificUser(username) => { 158 | unimplemented!("TODO: Get base directory for {:?}", username); 159 | } 160 | InstallTarget::Global => { 161 | // TODO+SUDO: Needs admin access to write to either. Should be 162 | // in a separate process that runs based on user password input. 163 | // Essentially the same UX as when shells try to run something 164 | // prefixed with `sudo` 165 | if cfg!(target_os = "macos") { 166 | Cow::Borrowed("/Library/Ocean/drops".as_ref()) 167 | } else { 168 | Cow::Borrowed("/usr/local/Ocean/drops".as_ref()) 169 | } 170 | } 171 | } 172 | 173 | #[cfg(windows)] 174 | unimplemented!("TODO: Write & test on Windows :)"); 175 | } 176 | } 177 | 178 | /// Indicates [`RtConfig::config`] failed. 179 | /// 180 | /// [`RtConfig::config`]: struct.RtConfig.html#method.config 181 | #[derive(Debug)] 182 | pub enum CreateError { 183 | /// Indicates `env::current_dir` failed. 184 | MissingCurrentDir(io::Error), 185 | /// Indicates `dirs::home_dir` failed. 186 | MissingUserHome, 187 | } 188 | 189 | impl fmt::Display for CreateError { 190 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 191 | match self { 192 | Self::MissingCurrentDir(error) => match error.kind() { 193 | io::ErrorKind::NotFound => { 194 | write!(f, "Current directory does not exist") 195 | } 196 | io::ErrorKind::PermissionDenied => write!( 197 | f, 198 | "Not enough permissions to access current directory", 199 | ), 200 | _ => write!(f, "Could not get current directory: {}", error), 201 | }, 202 | Self::MissingUserHome => { 203 | write!(f, "Could not get current user's home") 204 | } 205 | } 206 | } 207 | } 208 | 209 | impl Error for CreateError { 210 | fn source(&self) -> Option<&(dyn Error + 'static)> { 211 | match self { 212 | Self::MissingCurrentDir(error) => Some(error), 213 | Self::MissingUserHome => None, 214 | } 215 | } 216 | } 217 | 218 | /// An error indicating that the `PATH` environment variable was not found. 219 | #[derive(Clone, Debug, PartialEq, Eq)] 220 | pub struct MissingPathVar(()); 221 | 222 | impl fmt::Display for MissingPathVar { 223 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 224 | write!(f, "missing 'PATH' environment variable") 225 | } 226 | } 227 | 228 | impl Error for MissingPathVar {} 229 | 230 | /// An error indicating that the current executable was not found. 231 | #[derive(Clone, Debug, PartialEq, Eq)] 232 | pub struct MissingCurrentExe(()); 233 | 234 | impl fmt::Display for MissingCurrentExe { 235 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 236 | write!(f, "cannot find `ocean` executable") 237 | } 238 | } 239 | 240 | impl Error for MissingCurrentExe {} 241 | -------------------------------------------------------------------------------- /cli/src/cmd/install.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use oceanpkg::{ 3 | api, 4 | drop::{ 5 | name::{Name, Query}, 6 | Manifest, 7 | }, 8 | }; 9 | use std::{ 10 | fs::{self, File}, 11 | io::{BufWriter, Write}, 12 | mem, 13 | path::{Path, PathBuf}, 14 | process::{Command, Stdio}, 15 | }; 16 | 17 | pub const NAME: &str = "install"; 18 | 19 | pub fn cmd() -> App { 20 | SubCommand::with_name(NAME) 21 | .about("Downloads and installs a drop") 22 | .arg( 23 | Arg::user_flag() 24 | .help("Drop(s) will be available to a specific user"), 25 | ) 26 | .arg( 27 | Arg::global_flag() 28 | .help("Drop(s) will be globally available to all users"), 29 | ) 30 | .arg( 31 | Arg::with_name("drop") 32 | .help("The package(s) to install") 33 | .multiple(true) 34 | .required(true), 35 | ) 36 | .arg( 37 | Arg::with_name("with") 38 | .help("Include optional dependencies") 39 | .long("with") 40 | .takes_value(true) 41 | .value_name("dep") 42 | .multiple(true), 43 | ) 44 | .arg( 45 | Arg::with_name("without") 46 | .help("Exclude recommended dependencies") 47 | .long("without") 48 | .takes_value(true) 49 | .value_name("dep") 50 | .multiple(true), 51 | ) 52 | } 53 | 54 | pub fn run(config: &mut Config, matches: &ArgMatches) -> crate::Result { 55 | let with_deps: Vec<&Name> = matches 56 | .values_of("with") 57 | .map(name_values) 58 | .unwrap_or_default(); 59 | 60 | let without_deps: Vec<&Name> = matches 61 | .values_of("without") 62 | .map(name_values) 63 | .unwrap_or_default(); 64 | 65 | handle_conflicts(&with_deps, &without_deps); 66 | 67 | let install_target = matches.install_target(); 68 | println!("Installing for {:?}", install_target); 69 | 70 | struct VersionedQuery { 71 | scope: Option, 72 | name: S, 73 | version: S, 74 | } 75 | 76 | impl<'a> VersionedQuery<&'a str> { 77 | fn file_name(&self) -> String { 78 | format!("{}@{}", self.name, self.version) 79 | } 80 | } 81 | 82 | let mut successes = Vec::>::new(); 83 | 84 | if let Some(drops) = matches.values_of("drop") { 85 | for drop in drops { 86 | // TODO: Parse name 87 | println!("Installing \"{}\"...", drop); 88 | 89 | macro_rules! fail { 90 | ($error:expr) => {{ 91 | error!( 92 | "failed to install \"{}\": {} (line {})", 93 | drop, 94 | $error, 95 | line!() 96 | ); 97 | continue; 98 | }}; 99 | } 100 | 101 | let query = Query::<&str>::parse_liberal(drop); 102 | let download = match download(config, query) { 103 | Ok(dl) => dl, 104 | Err(error) => fail!(error), 105 | }; 106 | 107 | let drops_dir = config.rt.drops_dir(&install_target); 108 | let unpack_dir = { 109 | let mut dir = drops_dir.into_owned(); 110 | dir.push(query.scope.unwrap_or("core")); 111 | dir 112 | }; 113 | 114 | let manifest_path = match unpack(&download.path, &unpack_dir) { 115 | Ok(path) => unpack_dir.join(path), 116 | Err(error) => fail!(error), 117 | }; 118 | 119 | let manifest = match Manifest::read_toml_file(&manifest_path) { 120 | Ok(m) => m, 121 | Err(error) => fail!(error), 122 | }; 123 | 124 | let query = VersionedQuery { 125 | scope: query.scope, 126 | name: query.name, 127 | version: match query.version { 128 | Some(v) => v, 129 | None => { 130 | fn leak(string: String) -> &'static str { 131 | let slice = string.as_str() as *const str; 132 | mem::forget(string); 133 | unsafe { &*slice } 134 | } 135 | leak(manifest.meta.version.to_string()) 136 | } 137 | }, 138 | }; 139 | 140 | let drop_path = { 141 | let mut path = unpack_dir; 142 | path.push(query.file_name()); 143 | path 144 | }; 145 | 146 | let exe_path = drop_path.join(manifest.meta.exe_path()); 147 | if !exe_path.is_file() { 148 | fail!(failure::format_err!( 149 | "file not found: \"{}\"", 150 | exe_path.display(), 151 | )); 152 | } 153 | 154 | let chmod_status = 155 | Command::new("chmod").arg("+x").arg(&exe_path).status(); 156 | match chmod_status { 157 | Ok(status) => { 158 | if !status.success() { 159 | fail!(failure::format_err!( 160 | "failed to make \"{}\" executable", 161 | exe_path.display(), 162 | )); 163 | } 164 | } 165 | Err(error) => fail!(error), 166 | } 167 | 168 | successes.push(query); 169 | } 170 | } 171 | 172 | // Get duration immediately after shipping finishes. 173 | let elapsed = config.rt.time_elapsed(); 174 | 175 | if !successes.is_empty() { 176 | println!(); 177 | println!("Successfully installed:"); 178 | for query in successes { 179 | print!("- "); 180 | if let Some(scope) = query.scope { 181 | print!("{}/", scope); 182 | } 183 | print!("{} ({})", query.name, query.version); 184 | println!(); 185 | } 186 | } 187 | 188 | println!(); 189 | println!("Finished in {:?}", elapsed); 190 | Ok(()) 191 | } 192 | 193 | /// Converts `values` to a vector of `Name`s if they're all valid, or exits with 194 | /// an error code if any are not. 195 | fn name_values(values: clap::Values) -> Vec<&Name> { 196 | values 197 | .map(Name::new) 198 | .collect::, _>>() 199 | .unwrap_or_else(|err| exit_error!(err)) 200 | } 201 | 202 | /// Checks if `with` and `without` have any shared names, exiting with an error 203 | /// code if they do. 204 | fn handle_conflicts(with: &[&Name], without: &[&Name]) { 205 | let mut conflicts = without.iter().filter(|name| with.contains(name)); 206 | 207 | if let Some(first) = conflicts.next() { 208 | eprint!("Cannot be `--with` and `--without`: {}", first); 209 | for conflict in conflicts { 210 | eprint!(", {}", conflict); 211 | } 212 | eprintln!(); 213 | std::process::exit(1); 214 | } 215 | } 216 | 217 | struct Download { 218 | #[allow(unused)] 219 | file: File, 220 | path: PathBuf, 221 | } 222 | 223 | fn download(config: &Config, drop: Query<&str>) -> crate::Result { 224 | let tarball_path = config.rt.tarball_cache_path(drop); 225 | 226 | if let Some(parent) = tarball_path.parent() { 227 | fs::DirBuilder::new().recursive(true).create(parent)?; 228 | } 229 | 230 | let mut tarball_file = fs::OpenOptions::new() 231 | .create(true) 232 | .write(true) 233 | .read(true) 234 | .open(&tarball_path)?; 235 | 236 | { 237 | let mut buf = BufWriter::new(&mut tarball_file); 238 | api::v1::download_drop(drop, &mut buf)?; 239 | buf.flush()?; 240 | } 241 | 242 | Ok(Download { 243 | file: tarball_file, 244 | path: tarball_path, 245 | }) 246 | } 247 | 248 | // FIXME: Non-command unpacking fails. 249 | // fn unpack(tarball: &mut File, destination: &Path) -> crate::Result { 250 | // oceanpkg::archive::unpack_tarball(tarball, destination)?; 251 | // Ok(()) 252 | // } 253 | 254 | fn unpack(tarball: &Path, dir: &Path) -> crate::Result { 255 | assert!(tarball.exists(), "{:?} does not exist", tarball); 256 | 257 | fs::DirBuilder::new().recursive(true).create(dir)?; 258 | 259 | // Runs `gunzip -c $download_path | tar xopf -` 260 | let mut gunzip = Command::new("gunzip") 261 | .arg("-c") 262 | .arg(tarball) 263 | .current_dir(dir) 264 | .stdout(Stdio::piped()) 265 | .spawn()?; 266 | let gunzip_stdout = gunzip.stdout.take().ok_or_else(|| { 267 | failure::err_msg("Could not get stdout handle for `gunzip`") 268 | })?; 269 | let tar_status = Command::new("tar") 270 | .args(&["xopf", "-"]) 271 | .current_dir(dir) 272 | .stdin(gunzip_stdout) 273 | .status()?; 274 | 275 | failure::ensure!(tar_status.success(), "Failed to unpack download"); 276 | 277 | // Runs `gunzip -c $download_path | tar -t */Ocean.toml -` 278 | let mut gunzip = Command::new("gunzip") 279 | .arg("-c") 280 | .arg(tarball) 281 | .current_dir(dir) 282 | .stdout(Stdio::piped()) 283 | .spawn()?; 284 | let gunzip_stdout = gunzip.stdout.take().ok_or_else(|| { 285 | failure::err_msg("Could not get stdout handle for `gunzip`") 286 | })?; 287 | let tar_output = Command::new("tar") 288 | .args(&["-t", "*/Ocean.toml", "-"]) 289 | .current_dir(dir) 290 | .stdin(gunzip_stdout) 291 | .output()?; 292 | 293 | let mut manifest_path = String::from_utf8(tar_output.stdout)?; 294 | while manifest_path.ends_with(|c| c == '\n' || c == '\r') { 295 | manifest_path.pop(); 296 | } 297 | 298 | Ok(manifest_path) 299 | } 300 | -------------------------------------------------------------------------------- /lib/src/config/file.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for configuration files. 2 | 3 | #[cfg(unix)] 4 | use std::os::unix::ffi::OsStrExt; 5 | 6 | use shared::ext::BytesExt; 7 | use std::{ 8 | error::Error, 9 | fmt, 10 | fs::File, 11 | io, 12 | path::{Path, PathBuf}, 13 | }; 14 | 15 | const STEM: &str = "ocean"; 16 | const STEM_LEN: usize = 5; 17 | 18 | mod ext { 19 | pub const TOML: &str = "toml"; 20 | pub const JSON: &str = "json"; 21 | pub const YAML: &str = "yaml"; 22 | pub const YML: &str = "yml"; 23 | 24 | pub const MIN_LEN: usize = 3; 25 | pub const MAX_LEN: usize = 4; 26 | } 27 | 28 | /// Information for the configuration file. 29 | /// 30 | /// The configuration file always has a stem of "ocean" (case-insensitive). 31 | #[derive(Debug)] 32 | pub struct ConfigFile { 33 | /// The location of the configuration file. 34 | pub path: PathBuf, 35 | /// The format with which to parse the file. 36 | pub fmt: ConfigFileFmt, 37 | /// An open handle to the file at `path`. 38 | pub handle: Option, 39 | } 40 | 41 | impl ConfigFile { 42 | /// Locates the configuration file at `path`. 43 | pub fn find(path: &Path) -> Result> { 44 | const MIN_LEN: usize = STEM_LEN + 1 + ext::MIN_LEN; 45 | const MAX_LEN: usize = STEM_LEN + 1 + ext::MAX_LEN; 46 | 47 | // Converts `io::Result` to `Result`; needed for 48 | // `path` 49 | macro_rules! convert_result { 50 | ($result:expr) => { 51 | $result.map_err(|err| { 52 | let reason = NotFoundReason::Io(err); 53 | NotFound { reason, path } 54 | }) 55 | }; 56 | } 57 | 58 | for entry in convert_result!(path.read_dir())? { 59 | let entry = convert_result!(entry)?; 60 | let name = entry.file_name(); 61 | 62 | // Only do cheap checks 63 | match name.len() { 64 | MIN_LEN..=MAX_LEN => {} 65 | _ => continue, 66 | } 67 | 68 | #[cfg(unix)] 69 | let bytes = name.as_bytes(); 70 | 71 | // Needed since bytes can only be retrieved via `str` on non-Unix :( 72 | #[cfg(not(unix))] 73 | let bytes = if let Some(s) = name.to_str() { 74 | s.as_bytes() 75 | } else { 76 | continue; 77 | }; 78 | 79 | // SAFETY: We call `.get_unchecked()` because the optimizer 80 | // apparently doesn't know that `bytes.len() == name.len()` 81 | let stem = unsafe { bytes.get_unchecked(..STEM_LEN) }; 82 | if !stem.matches_special_lowercase(STEM) { 83 | continue; 84 | } 85 | 86 | // SAFETY: See above^ 87 | if unsafe { *bytes.get_unchecked(STEM_LEN) } != b'.' { 88 | continue; 89 | } 90 | 91 | // SAFETY: See above^ 92 | let ext = unsafe { bytes.get_unchecked((STEM_LEN + 1)..) }; 93 | 94 | if let Some(fmt) = ConfigFileFmt::from_bytes(ext) { 95 | return Ok(ConfigFile { 96 | path: path.join(name), 97 | fmt, 98 | handle: None, 99 | }); 100 | } else { 101 | continue; 102 | }; 103 | } 104 | 105 | Err(NotFound { 106 | reason: NotFoundReason::NoMatch, 107 | path, 108 | }) 109 | } 110 | } 111 | 112 | /// The format of the configuration file. 113 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 114 | pub enum ConfigFileFmt { 115 | /// [Tom's Obvious, Minimal Language](https://github.com/toml-lang/toml). 116 | /// 117 | /// Extension: `toml` 118 | Toml, 119 | /// [JavaScript Object Notation](https://json.org). 120 | /// 121 | /// Extension: `json` 122 | Json, 123 | /// [YAML Ain't Markup Language](http://yaml.org). 124 | /// 125 | /// Extensions: `yaml`, `yml` 126 | Yaml, 127 | } 128 | 129 | impl ConfigFileFmt { 130 | /// Returns the corresponding variant for the file extension, going from 131 | /// TOML to JSON to YAML in order. 132 | /// 133 | /// This expects `bytes` to be ASCII/UTF-8 and will simply fail with other 134 | /// encodings. 135 | pub fn from_bytes(bytes: &[u8]) -> Option { 136 | // Perform a case-insensitive match on each extension and assign the 137 | // corresponding `ConfigFileFmt` variant, moving onto the next entry if 138 | // none match 139 | macro_rules! handle_ext { 140 | ($($fmt:ident => $($ext:expr),+;)+) => { 141 | $(if $(bytes.matches_special_lowercase($ext))||+ { 142 | Some(ConfigFileFmt::$fmt) 143 | } else)+ { 144 | None 145 | } 146 | }; 147 | } 148 | 149 | handle_ext! { 150 | Toml => ext::TOML; 151 | Json => ext::JSON; 152 | Yaml => ext::YAML, ext::YML; 153 | } 154 | } 155 | 156 | /// Returns the corresponding variant for the file pointed to at `path` 157 | /// based on its extension, going from TOML to JSON to YAML in order. 158 | pub fn from_path(path: &Path) -> Option { 159 | let ext = path.extension()?; 160 | match ext.len() { 161 | // Range allows for doing only cheap UTF-8 checks on non-Unix 162 | ext::MIN_LEN..=ext::MAX_LEN => { 163 | // Assume ASCII extension 164 | #[cfg(unix)] 165 | let ext = ext.as_bytes(); 166 | 167 | #[cfg(not(unix))] 168 | let ext = ext.to_str()?.as_bytes(); 169 | 170 | ConfigFileFmt::from_bytes(ext) 171 | } 172 | _ => None, 173 | } 174 | } 175 | } 176 | 177 | /// The underlying cause for `ConfigFile::find` to return 178 | /// [`NotFound`](struct.NotFound.html). 179 | #[derive(Debug)] 180 | pub enum NotFoundReason { 181 | /// Could not read a directory or entries within the directory. 182 | Io(io::Error), 183 | /// The Ocean configuration file could not be found in the directory it's 184 | /// expected to be in. 185 | NoMatch, 186 | } 187 | 188 | impl From for NotFoundReason { 189 | fn from(error: io::Error) -> Self { 190 | NotFoundReason::Io(error) 191 | } 192 | } 193 | 194 | /// The error returned by `ConfigFile::find`. 195 | #[derive(Debug)] 196 | pub struct NotFound<'a> { 197 | /// The underlying cause. 198 | pub reason: NotFoundReason, 199 | /// The path being searched when the error ocurred. 200 | pub path: &'a Path, 201 | } 202 | 203 | impl Error for NotFound<'_> {} 204 | 205 | impl fmt::Display for NotFound<'_> { 206 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 207 | match &self.reason { 208 | NotFoundReason::Io(err) => write!(f, "{} for {:?}", err, self.path), 209 | NotFoundReason::NoMatch => write!( 210 | f, 211 | "No TOML, JSON, or YAML file named \"ocean\" found in \"{}\"", 212 | self.path.display() 213 | ), 214 | } 215 | } 216 | } 217 | 218 | #[cfg(test)] 219 | mod tests { 220 | use self::ConfigFileFmt::*; 221 | use super::*; 222 | 223 | static PAIRS: &[(ConfigFileFmt, &[&str])] = &[ 224 | (Toml, &[ext::TOML]), 225 | (Json, &[ext::JSON]), 226 | (Yaml, &[ext::YAML, ext::YML]), 227 | ]; 228 | 229 | #[test] 230 | fn find_cfg_file() { 231 | let dir = tempfile::tempdir().unwrap(); 232 | 233 | match ConfigFile::find(dir.path()) { 234 | Ok(file) => panic!("Found unexpected config {:?}", file), 235 | Err(err) => match err.reason { 236 | NotFoundReason::NoMatch => {} 237 | NotFoundReason::Io(err) => panic!("{}", err), 238 | }, 239 | } 240 | 241 | for &(fmt, exts) in PAIRS { 242 | for &ext in exts { 243 | let cfg_name = format!("{}.{}", STEM, ext); 244 | let upper = cfg_name.to_uppercase(); 245 | let lower = cfg_name.to_lowercase(); 246 | 247 | for cfg_name in &[lower, upper] { 248 | let cfg_path = dir.path().join(&cfg_name); 249 | std::fs::File::create(&cfg_path).unwrap(); 250 | 251 | let cfg_file = ConfigFile::find(dir.path()).unwrap(); 252 | assert_eq!(cfg_file.path, cfg_path); 253 | assert_eq!(cfg_file.fmt, fmt); 254 | 255 | std::fs::remove_file(cfg_path).unwrap(); 256 | } 257 | } 258 | } 259 | 260 | #[cfg(unix)] 261 | { 262 | use std::os::unix::fs::DirBuilderExt; 263 | 264 | let no_read = dir.path().join("no_read"); 265 | std::fs::DirBuilder::new() 266 | .mode(0) // no permissions 267 | .create(&no_read) 268 | .unwrap(); 269 | 270 | let exp_err = std::io::ErrorKind::PermissionDenied; 271 | match ConfigFile::find(&no_read) { 272 | Ok(file) => panic!("Found unexpected config {:?}", file), 273 | Err(err) => match err.reason { 274 | NotFoundReason::NoMatch => panic!("Should emit IO error"), 275 | NotFoundReason::Io(err) => assert_eq!(err.kind(), exp_err), 276 | }, 277 | } 278 | } 279 | } 280 | 281 | #[test] 282 | fn fmt_from_path() { 283 | let prefixes: &[_] = &["", "/", "./", "/xyz/"]; 284 | 285 | for &(fmt, exts) in PAIRS { 286 | for ext in exts { 287 | for prefix in prefixes { 288 | let path = 289 | PathBuf::from(format!("{}{}.{}", prefix, STEM, ext)); 290 | assert_eq!(ConfigFileFmt::from_path(&path).unwrap(), fmt); 291 | } 292 | } 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /lib/src/drop/name/scoped.rs: -------------------------------------------------------------------------------- 1 | //! A drop name in the format `/`. 2 | 3 | use super::{Name, Query, ValidateError}; 4 | use std::{convert::TryInto, fmt}; 5 | 6 | /// Name in the format `/`. 7 | #[derive(Clone, Copy, Debug, Eq, PartialOrd, Ord, Hash)] 8 | #[repr(C)] // For `as_names` to always have the same order 9 | pub struct ScopedName { 10 | /// The namespace of the drop. 11 | pub scope: Name, 12 | /// The drop's given name. 13 | pub name: Name, 14 | } 15 | 16 | assert_eq_size!(ScopedName>, ScopedName<&Name>); 17 | assert_eq_align!(ScopedName>, ScopedName<&Name>); 18 | 19 | impl> From<[B; 2]> for ScopedName { 20 | #[inline] 21 | fn from([scope, name]: [B; 2]) -> Self { 22 | Self::new(scope, name) 23 | } 24 | } 25 | 26 | impl From<(A, B)> for ScopedName 27 | where 28 | A: Into, 29 | B: Into, 30 | { 31 | #[inline] 32 | fn from((scope, name): (A, B)) -> Self { 33 | Self::new(scope, name) 34 | } 35 | } 36 | 37 | impl PartialEq> for ScopedName 38 | where 39 | A: PartialEq, 40 | { 41 | #[inline] 42 | fn eq(&self, other: &ScopedName) -> bool { 43 | self.scope == other.scope && self.name == other.name 44 | } 45 | } 46 | 47 | impl> PartialEq for ScopedName { 48 | fn eq(&self, s: &str) -> bool { 49 | let mut parts = s.splitn(2, '/'); 50 | match (parts.next(), parts.next()) { 51 | (Some(scope), Some(name)) => { 52 | self.scope.as_ref() == scope && self.name.as_ref() == name 53 | } 54 | _ => false, 55 | } 56 | } 57 | } 58 | 59 | // Seems redundant but required to make `assert_eq!` prettier. 60 | impl> PartialEq<&str> for ScopedName { 61 | #[inline] 62 | fn eq(&self, s: &&str) -> bool { 63 | *self == **s 64 | } 65 | } 66 | 67 | impl> PartialEq> for str { 68 | #[inline] 69 | fn eq(&self, n: &ScopedName) -> bool { 70 | n == self 71 | } 72 | } 73 | 74 | // Seems redundant but required to make `assert_eq!` prettier. 75 | impl> PartialEq> for &str { 76 | #[inline] 77 | fn eq(&self, n: &ScopedName) -> bool { 78 | n == self 79 | } 80 | } 81 | 82 | impl fmt::Display for ScopedName { 83 | #[inline] 84 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 85 | write!(f, "{}/{}", self.scope, self.name) 86 | } 87 | } 88 | 89 | impl ScopedName { 90 | /// Creates a new instance from `scope` and `name`. 91 | #[inline] 92 | pub fn new(scope: A, name: B) -> Self 93 | where 94 | A: Into, 95 | B: Into, 96 | { 97 | Self { 98 | scope: scope.into(), 99 | name: name.into(), 100 | } 101 | } 102 | 103 | /// Attempts to create a new instance by parsing `name`. 104 | #[inline] 105 | pub fn parse(name: A) -> Result> 106 | where 107 | A: TryInto>, 108 | { 109 | name.try_into() 110 | } 111 | 112 | /// Converts `self` into a new `ScopedName` by performing an [`Into`] 113 | /// conversion over all fields. 114 | /// 115 | /// [`Into`]: https://doc.rust-lang.org/std/convert/trait.Into.html 116 | #[inline] 117 | pub fn cast(self) -> ScopedName 118 | where 119 | N: Into, 120 | { 121 | self.map(Into::into) 122 | } 123 | 124 | /// Converts `self` into a new `Query` by performing an [`Into`] conversion 125 | /// over all fields. 126 | /// 127 | /// [`Into`]: https://doc.rust-lang.org/std/convert/trait.Into.html 128 | pub fn try_cast(self) -> Result, ParseError> 129 | where 130 | N: TryInto, 131 | { 132 | let scope = match self.scope.try_into() { 133 | Err(error) => return Err(ParseError::Scope(error)), 134 | Ok(scope) => scope, 135 | }; 136 | let name = match self.name.try_into() { 137 | Err(error) => return Err(ParseError::Name(error)), 138 | Ok(name) => name, 139 | }; 140 | Ok(ScopedName { scope, name }) 141 | } 142 | 143 | /// Takes shared references to the fields of this name. 144 | #[inline] 145 | pub fn as_ref(&self) -> ScopedName<&N> { 146 | ScopedName { 147 | scope: &self.scope, 148 | name: &self.name, 149 | } 150 | } 151 | 152 | /// Takes a shared reference to the fields of this name as type `A`. 153 | #[inline] 154 | pub fn to_ref(&self) -> ScopedName<&A> 155 | where 156 | N: AsRef, 157 | A: ?Sized, 158 | { 159 | self.as_ref().map(AsRef::as_ref) 160 | } 161 | 162 | /// Takes mutable references to the fields of this name. 163 | #[inline] 164 | pub fn as_mut(&mut self) -> ScopedName<&mut N> { 165 | ScopedName { 166 | scope: &mut self.scope, 167 | name: &mut self.name, 168 | } 169 | } 170 | 171 | /// Takes a mutable references to the fields of this name as type `A`. 172 | #[inline] 173 | pub fn to_mut(&mut self) -> ScopedName<&mut A> 174 | where 175 | N: AsMut, 176 | A: ?Sized, 177 | { 178 | self.as_mut().map(AsMut::as_mut) 179 | } 180 | 181 | /// Creates a new `ScopedName` by mapping the function over the fields of 182 | /// `self`. 183 | #[inline] 184 | pub fn map(self, mut f: F) -> ScopedName 185 | where 186 | F: FnMut(N) -> A, 187 | { 188 | ScopedName { 189 | scope: f(self.scope), 190 | name: f(self.name), 191 | } 192 | } 193 | 194 | /// Converts `self` into a `Query`. 195 | #[inline] 196 | pub fn into_query(self) -> Query { 197 | self.into() 198 | } 199 | 200 | /// Converts `self` into a `Query` with the specified `version`. 201 | #[inline] 202 | pub fn into_query_with(self, version: V) -> Query { 203 | Query { 204 | scope: Some(self.scope), 205 | name: self.name, 206 | version: Some(version), 207 | } 208 | } 209 | 210 | /// Converts `self` into an array of names. 211 | #[inline] 212 | pub fn as_names_array(&self) -> &[N; 2] { 213 | // SAFETY: This type consists of exactly two `N`s 214 | unsafe { &*(self as *const Self as *const [N; 2]) } 215 | } 216 | 217 | /// Converts `self` into a slice of names. 218 | #[inline] 219 | pub fn as_names_slice(&self) -> &[N] { 220 | self.as_names_array() 221 | } 222 | } 223 | 224 | impl<'n, N: ?Sized> ScopedName<&'n N> { 225 | /// Returns the result of calling [`Clone::clone`] on the fields of `self`. 226 | /// 227 | /// [`Clone::clone`]: https://doc.rust-lang.org/std/clone/trait.Clone.html#tymethod.clone 228 | #[inline] 229 | pub fn cloned(&self) -> ScopedName 230 | where 231 | N: Clone, 232 | { 233 | self.map(Clone::clone) 234 | } 235 | 236 | /// Returns the result of calling [`ToOwned::to_owned`] on the fields of 237 | /// `self`. 238 | /// 239 | /// # Examples 240 | /// 241 | /// ``` 242 | /// use oceanpkg::drop::name::ScopedName; 243 | /// 244 | /// let name: ScopedName<&str> = ScopedName::new("core", "wget"); 245 | /// let owned: ScopedName = name.to_owned(); 246 | /// 247 | /// assert_eq!(name, owned); 248 | /// ``` 249 | /// 250 | /// [`ToOwned::to_owned`]: https://doc.rust-lang.org/std/borrow/trait.ToOwned.html#tymethod.to_owned 251 | #[inline] 252 | pub fn to_owned(&self) -> ScopedName 253 | where 254 | N: ToOwned, 255 | { 256 | self.map(ToOwned::to_owned) 257 | } 258 | } 259 | 260 | impl<'n> ScopedName<&'n Name> { 261 | /// Creates a new instance in the `core` namespace. 262 | #[inline] 263 | pub const fn core(name: &'n Name) -> Self { 264 | Self { 265 | scope: Name::CORE, 266 | name, 267 | } 268 | } 269 | 270 | /// Creates a new instance in the `ocean` namespace. 271 | #[inline] 272 | pub const fn ocean(name: &'n Name) -> Self { 273 | Self { 274 | scope: Name::OCEAN, 275 | name, 276 | } 277 | } 278 | 279 | /// Creates a new instance by verifying `scope` and `name`. 280 | #[inline] 281 | pub fn from_pair( 282 | scope: &'n S, 283 | name: &'n N, 284 | ) -> Result> 285 | where 286 | S: ?Sized + AsRef<[u8]>, 287 | N: ?Sized + AsRef<[u8]>, 288 | { 289 | match Name::new(scope.as_ref()) { 290 | Ok(scope) => match Name::new(name.as_ref()) { 291 | Ok(name) => Ok(Self { scope, name }), 292 | Err(err) => Err(ParseError::Name(err)), 293 | }, 294 | Err(err) => Err(ParseError::Scope(err)), 295 | } 296 | } 297 | 298 | /// Creates a new instance without attempting to verify `scope` or `name`. 299 | #[inline] 300 | #[allow(clippy::missing_safety_doc)] // TODO: Add `# Safety` section 301 | pub unsafe fn from_pair_unchecked(scope: &'n S, name: &'n N) -> Self 302 | where 303 | S: ?Sized + AsRef<[u8]>, 304 | N: ?Sized + AsRef<[u8]>, 305 | { 306 | Self { 307 | scope: Name::new_unchecked(scope), 308 | name: Name::new_unchecked(name), 309 | } 310 | } 311 | } 312 | 313 | /// Error returned when parsing a [`ScopedName`](struct.ScopedName.html). 314 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 315 | pub enum ParseError { 316 | /// Could not parse the scope (what comes before the separator). 317 | Scope(NameError), 318 | /// Could not parse the drop's name itself. 319 | Name(NameError), 320 | /// The separator character ('/') was not found in a scoped name. 321 | MissingSeparator, 322 | } 323 | 324 | impl fmt::Display for ParseError { 325 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 326 | match self { 327 | Self::Scope(error) => write!(f, "failed to parse scope: {}", error), 328 | Self::Name(error) => write!(f, "failed to parse name: {}", error), 329 | Self::MissingSeparator => { 330 | write!(f, "missing '/' separator in scoped name") 331 | } 332 | } 333 | } 334 | } 335 | 336 | #[cfg(test)] 337 | mod tests { 338 | use super::*; 339 | 340 | #[test] 341 | fn eq_str() { 342 | fn test(name: ScopedName<&Name>) { 343 | fn bad_names(name: &str) -> Vec { 344 | let name = name.to_string(); 345 | vec![ 346 | name.to_uppercase(), 347 | format!("{}/", name), 348 | format!("/{}", name), 349 | format!("/{}/", name), 350 | name.replace("/", ""), 351 | ] 352 | } 353 | 354 | let name_string = name.to_string(); 355 | assert_eq!(name, *name_string); 356 | 357 | for bad_name in bad_names(&name_string) { 358 | assert_ne!(name, *bad_name); 359 | } 360 | } 361 | 362 | let names = Name::RESERVED_SCOPES; 363 | 364 | for &name in names { 365 | for &scope in names { 366 | test(ScopedName { scope, name }); 367 | } 368 | } 369 | } 370 | } 371 | --------------------------------------------------------------------------------