Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.vscode

/target
Cargo.lock
7 changes: 0 additions & 7 deletions Cargo.lock

This file was deleted.

26 changes: 26 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,29 @@
name = "snapd"
version = "0.1.0"
edition = "2021"

[dependencies]
async-trait = "0.1.77"
deadpool = { version = "0.10.0", features = [
"managed",
"async-trait",
"rt_tokio_1",
], default-features = false }
http = "1.0.0"
http-body-util = "0.1.0"
hyper = { version = "1.1.0", default-features = false, features = [
"client",
"http1",
] }
pin-project = "1.1.4"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
thiserror = "1.0.56"
tokio = { version = "1.35.1", default-features = false, features = [
"net",
"io-util",
] }
url = "2.5.0"

[dev-dependencies]
tokio = { version = "1.35.1", default-features = false, features = ["full"] }
172 changes: 172 additions & 0 deletions src/api/alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use url::Url;

use crate::SnapdClient;

use super::{snap_str_newtype, App, Get, JsonPayload, SnapCommand, SnapName};

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case", tag = "action")]
pub enum AliasCommand<'a> {
Alias {
#[serde(borrow)]
snap: SnapName<'a>,
#[serde(borrow)]
alias: SnapAlias<'a>,
#[serde(borrow, skip_serializing_if = "Option::is_none")]
app: Option<App<'a>>,
},
Unalias {
#[serde(borrow, skip_serializing_if = "Option::is_none")]
snap: Option<SnapName<'a>>,
#[serde(borrow)]
alias: SnapAlias<'a>,
#[serde(borrow, skip_serializing_if = "Option::is_none")]
app: Option<App<'a>>,
},
Prefer {
#[serde(borrow)]
snap: SnapName<'a>,
#[serde(borrow)]
alias: SnapAlias<'a>,
#[serde(borrow, skip_serializing_if = "Option::is_none")]
app: Option<App<'a>>,
},
}

#[derive(Debug, Copy, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub struct GetAliases;

impl Get for GetAliases {
type Payload<'p> = JsonPayload<'p, Aliases<'p>>;
type Client = SnapdClient;

fn url(&self, base_url: Url) -> Url {
base_url.join("/v2/aliases").unwrap()
}
}

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Aliases<'a>(
#[serde(borrow)] HashMap<SnapName<'a>, HashMap<SnapAlias<'a>, AliasInfo<'a>>>,
);

#[derive(Clone, Serialize, Deserialize, Debug, Hash, PartialEq, Eq)]
pub struct AliasInfo<'a> {
#[serde(borrow)]
command: SnapCommand<'a, 'a>,
#[serde(flatten, borrow)]
status: AliasStatus<'a>,
}

#[derive(Clone, Serialize, Deserialize, Debug, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case", tag = "status")]
pub enum AliasStatus<'a> {
Auto {
#[serde(rename = "auto", borrow)]
app_name: App<'a>,
},
Manual {
#[serde(rename = "manual", borrow)]
app_name: App<'a>,
},
Disabled,
}

snap_str_newtype! {
SnapAlias
}

#[cfg(test)]
mod test {
use super::*;
use serde_json;

#[test]
fn deserialize_info() {
let json = r#"
{
"snap":
{
"alias1":
{
"command": "snap.app",
"status": "auto",
"auto": "app"
},
"alias2":
{
"command": "foo",
"status": "manual",
"manual": "app1"
}
}
}
"#;

let mut alias_map = HashMap::with_capacity(2);
alias_map.insert(
"alias1".into(),
AliasInfo {
command: SnapCommand::from_convertible("snap", "app"),
status: AliasStatus::Auto {
app_name: "app".into(),
},
},
);

alias_map.insert(
"alias2".into(),
AliasInfo {
command: SnapCommand::from_raw("foo"),
status: AliasStatus::Manual {
app_name: "app1".into(),
},
},
);

let mut snap_map = HashMap::with_capacity(1);
snap_map.insert("snap".into(), alias_map);

let aliases = Aliases(snap_map);

assert_eq!(
aliases,
serde_json::from_str(json).expect("could not decode alias response json")
)
}

#[test]
fn serialize_command_no_app() {
let command = AliasCommand::Alias {
snap: "steam".into(),
alias: "games".into(),
app: None,
};

let expected = r#"{"action":"alias","snap":"steam","alias":"games"}"#;

assert_eq!(
serde_json::to_string(&command).expect("could not serialize"),
expected
);
}

#[test]
fn serialize_command_app() {
let command = AliasCommand::Alias {
snap: "steam".into(),
alias: "vulkan".into(),
app: Some("vkinfo".into()),
};

let expected = r#"{"action":"alias","snap":"steam","alias":"vulkan","app":"vkinfo"}"#;

assert_eq!(
serde_json::to_string(&command).expect("could not serialize"),
expected
);
}
}
71 changes: 71 additions & 0 deletions src/api/assertions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::{convert::Infallible, io::BufRead, marker::PhantomData};

use http_body_util::Collected;
use hyper::body::Bytes;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use super::{Payload, SnapId, SnapName, ToOwnedInner};

#[derive(Clone, Debug, Error)]
pub enum SnapDeclarationError {
#[error("didn't find a snap with the given id")]
NoSnapsFound,
}

#[derive(Clone, Default, Hash, Eq, PartialEq, Serialize, Deserialize, Debug)]
pub struct SnapDeclaration<'a> {
#[serde(borrow)]
pub(crate) snap_id: SnapId<'a>,
#[serde(borrow)]
pub(crate) snap_name: SnapName<'a>,
}

pub struct DeclarationAssertionPayload<'de> {
pub data: Bytes,
pd: PhantomData<&'de SnapDeclaration<'de>>,
}

impl<'de> DeclarationAssertionPayload<'de> {
pub(crate) fn parse(&'de self) -> Result<SnapDeclaration<'de>, Infallible> {
let mut declaration = SnapDeclaration::default();
// Super annoying, need to fix this, but the assertion response is a huge mess to begin with
for line in self.data.lines().map(|v| v.unwrap()) {
if line.starts_with("snap-name") {
let name: SnapName = line.split_once(':').unwrap().1.trim().into();
declaration.snap_name = name.to_owned_inner();
}

if line.starts_with("snap-id") {
let id: SnapId = line.split_once(':').unwrap().1.trim().into();
declaration.snap_id = id.to_owned_inner();
}
}

Ok(declaration)
}
}

impl<'de> Payload<'de> for DeclarationAssertionPayload<'de> {
type Parsed<'a> = SnapDeclaration<'a> where Self: 'a, 'a: 'de;

fn parse<'a>(&'a self) -> Self::Parsed<'a>
where
'a: 'de,
Self: 'a,
{
self.parse().expect(
"error in parsing assertion response, this is an \
internal snapd-rs bug, please file an issue",
)
}
}

impl<'de> From<Collected<Bytes>> for DeclarationAssertionPayload<'de> {
fn from(data: Collected<Bytes>) -> Self {
Self {
data: data.to_bytes(),
pd: PhantomData,
}
}
}
61 changes: 61 additions & 0 deletions src/api/convenience/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use url::Url;

use crate::{GetClient, SnapdClient, SnapdClientError};

use super::assertions::DeclarationAssertionPayload;
use super::{Get, SnapId, SnapName, ToOwnedInner};

pub use crate::api::assertions::SnapDeclarationError;

#[derive(Clone, Default, Hash, Eq, PartialEq, Debug)]
pub struct SnapNameFromId<'a> {
pub name: SnapId<'a>,
}

impl<'a> SnapNameFromId<'a> {
pub async fn get_name(
id: SnapId<'_>,
client: &SnapdClient,
) -> Result<SnapName<'static>, SnapdClientError> {
let response = client.get(&SnapNameFromId { name: id }).await?;
let declaration = response.parse().unwrap();

if declaration.snap_name.as_ref() == "" {
return Err(SnapDeclarationError::NoSnapsFound)?;
};

Ok(declaration.snap_name.to_owned_inner())
}
}

impl<'a> Get for SnapNameFromId<'a> {
type Payload<'de> = DeclarationAssertionPayload<'de>;

type Client = SnapdClient;

fn url(&self, base_url: Url) -> Url {
base_url
.join(&format!(
// TODO: understand implications of `series=16` but it seems to work for
// a wide variety of snaps I've tested ATM
"/v2/assertions/snap-declaration?series=16&remote=true&snap-id={}",
self.name
))
.unwrap()
}
}

#[cfg(test)]
mod test {
use super::*;

#[tokio::test]
async fn name_from_id() {
let client = SnapdClient::default();

let id = SnapNameFromId::get_name("NeoQngJVBf2wKC48bxnF2xqmfEFGdVnx".into(), &client)
.await
.unwrap();
assert_eq!("steam", id.as_ref())
}
}
Loading