diff --git a/Cargo.lock b/Cargo.lock
index a590d479c..85aac6e54 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -372,6 +372,7 @@ version = "1.5.0"
dependencies = [
"ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-util 1.5.0",
+ "fetch 0.1.0",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/ethcore/hash-fetch/Cargo.toml b/ethcore/hash-fetch/Cargo.toml
index f1a65bfc8..4fb724c08 100644
--- a/ethcore/hash-fetch/Cargo.toml
+++ b/ethcore/hash-fetch/Cargo.toml
@@ -11,4 +11,5 @@ log = "0.3"
rustc-serialize = "0.3"
ethabi = "0.2.2"
mime_guess = "1.6.1"
+fetch = { path = "../../util/fetch" }
ethcore-util = { path = "../../util" }
diff --git a/ethcore/hash-fetch/src/client.rs b/ethcore/hash-fetch/src/client.rs
new file mode 100644
index 000000000..f5d19afa5
--- /dev/null
+++ b/ethcore/hash-fetch/src/client.rs
@@ -0,0 +1,114 @@
+// Copyright 2015, 2016 Ethcore (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+//! Hash-addressed content resolver & fetcher.
+
+use std::{io, fs};
+use std::sync::Arc;
+use std::path::PathBuf;
+
+use util::{Mutex, H256, sha3};
+use fetch::{Fetch, FetchError, Client as FetchClient};
+
+use urlhint::{ContractClient, URLHintContract, URLHint, URLHintResult};
+
+/// API for fetching by hash.
+pub trait HashFetch {
+ /// Fetch hash-addressed content.
+ /// Parameters:
+ /// 1. `hash` - content hash
+ /// 2. `on_done` - callback function invoked when the content is ready (or there was error during fetch)
+ ///
+ /// This function may fail immediately when fetch cannot be initialized or content cannot be resolved.
+ fn fetch(&self, hash: H256, on_done: Box) + Send>) -> Result<(), Error>;
+}
+
+/// Hash-fetching error.
+#[derive(Debug)]
+pub enum Error {
+ /// Hash could not be resolved to a valid content address.
+ NoResolution,
+ /// Downloaded content hash does not match.
+ HashMismatch { expected: H256, got: H256 },
+ /// IO Error while validating hash.
+ IO(io::Error),
+ /// Error during fetch.
+ Fetch(FetchError),
+}
+
+impl From for Error {
+ fn from(error: FetchError) -> Self {
+ Error::Fetch(error)
+ }
+}
+
+impl From for Error {
+ fn from(error: io::Error) -> Self {
+ Error::IO(error)
+ }
+}
+
+/// Default Hash-fetching client using on-chain contract to resolve hashes to URLs.
+pub struct Client {
+ contract: URLHintContract,
+ fetch: Mutex,
+}
+
+impl Client {
+ /// Creates new instance of the `Client` given on-chain contract client.
+ pub fn new(contract: Arc) -> Self {
+ Client {
+ contract: URLHintContract::new(contract),
+ fetch: Mutex::new(FetchClient::default()),
+ }
+ }
+}
+
+impl HashFetch for Client {
+ fn fetch(&self, hash: H256, on_done: Box) + Send>) -> Result<(), Error> {
+ debug!(target: "dapps", "Fetching: {:?}", hash);
+
+ let url = try!(
+ self.contract.resolve(hash.to_vec()).map(|content| match content {
+ URLHintResult::Dapp(dapp) => {
+ dapp.url()
+ },
+ URLHintResult::Content(content) => {
+ content.url
+ },
+ }).ok_or_else(|| Error::NoResolution)
+ );
+
+ debug!(target: "dapps", "Resolved {:?} to {:?}. Fetching...", hash, url);
+
+ self.fetch.lock().request_async(&url, Default::default(), Box::new(move |result| {
+ fn validate_hash(hash: H256, result: Result) -> Result {
+ let path = try!(result);
+ let mut file_reader = io::BufReader::new(try!(fs::File::open(&path)));
+ let content_hash = try!(sha3(&mut file_reader));
+
+ if content_hash != hash {
+ Err(Error::HashMismatch{ got: content_hash, expected: hash })
+ } else {
+ Ok(path)
+ }
+ }
+
+ debug!(target: "dapps", "Content fetched, validating hash ({:?})", hash);
+ on_done(validate_hash(hash, result))
+ })).map_err(Into::into)
+ }
+}
diff --git a/ethcore/hash-fetch/src/lib.rs b/ethcore/hash-fetch/src/lib.rs
index ac613a558..ffb74b260 100644
--- a/ethcore/hash-fetch/src/lib.rs
+++ b/ethcore/hash-fetch/src/lib.rs
@@ -14,12 +14,20 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+//! Hash-addressed content resolver & fetcher.
+
+#![warn(missing_docs)]
+
#[macro_use]
extern crate log;
extern crate rustc_serialize;
extern crate mime_guess;
extern crate ethabi;
extern crate ethcore_util as util;
+extern crate fetch;
+
+mod client;
pub mod urlhint;
+pub use client::{HashFetch, Client};
diff --git a/ethcore/hash-fetch/src/urlhint.rs b/ethcore/hash-fetch/src/urlhint.rs
index b7f905144..9cbd13b1e 100644
--- a/ethcore/hash-fetch/src/urlhint.rs
+++ b/ethcore/hash-fetch/src/urlhint.rs
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+//! URLHint Contract
+
use std::fmt;
use std::sync::Arc;
use rustc_serialize::hex::ToHex;
@@ -24,15 +26,30 @@ use util::{Address, Bytes, Hashable};
const COMMIT_LEN: usize = 20;
+/// RAW Contract interface.
+/// Should execute transaction using current blockchain state.
+pub trait ContractClient: Send + Sync {
+ /// Get registrar address
+ fn registrar(&self) -> Result;
+ /// Call Contract
+ fn call(&self, address: Address, data: Bytes) -> Result;
+}
+
+/// Github-hosted dapp.
#[derive(Debug, PartialEq)]
pub struct GithubApp {
+ /// Github Account
pub account: String,
+ /// Github Repository
pub repo: String,
+ /// Commit on Github
pub commit: [u8;COMMIT_LEN],
+ /// Dapp owner address
pub owner: Address,
}
impl GithubApp {
+ /// Returns URL of this Github-hosted dapp package.
pub fn url(&self) -> String {
// Since https fetcher doesn't support redirections we use direct link
// format!("https://github.com/{}/{}/archive/{}.zip", self.account, self.repo, self.commit.to_hex())
@@ -53,22 +70,17 @@ impl GithubApp {
}
}
+/// Hash-Addressed Content
#[derive(Debug, PartialEq)]
pub struct Content {
+ /// URL of the content
pub url: String,
+ /// MIME type of the content
pub mime: String,
+ /// Content owner address
pub owner: Address,
}
-/// RAW Contract interface.
-/// Should execute transaction using current blockchain state.
-pub trait ContractClient: Send + Sync {
- /// Get registrar address
- fn registrar(&self) -> Result;
- /// Call Contract
- fn call(&self, address: Address, data: Bytes) -> Result;
-}
-
/// Result of resolving id to URL
#[derive(Debug, PartialEq)]
pub enum URLHintResult {
@@ -84,6 +96,7 @@ pub trait URLHint {
fn resolve(&self, id: Bytes) -> Option;
}
+/// `URLHintContract` API
pub struct URLHintContract {
urlhint: Contract,
registrar: Contract,
@@ -91,6 +104,7 @@ pub struct URLHintContract {
}
impl URLHintContract {
+ /// Creates new `URLHintContract`
pub fn new(client: Arc) -> Self {
let urlhint = Interface::load(include_bytes!("../res/urlhint.json")).expect("urlhint.json is valid ABI");
let registrar = Interface::load(include_bytes!("../res/registrar.json")).expect("registrar.json is valid ABI");
@@ -244,11 +258,6 @@ fn guess_mime_type(url: &str) -> Option {
})
}
-#[cfg(test)]
-pub fn test_guess_mime_type(url: &str) -> Option {
- guess_mime_type(url)
-}
-
fn as_string(e: T) -> String {
format!("{:?}", e)
}
@@ -260,6 +269,7 @@ mod tests {
use rustc_serialize::hex::FromHex;
use super::*;
+ use super::guess_mime_type;
use util::{Bytes, Address, Mutex, ToPretty};
struct FakeRegistrar {
@@ -390,10 +400,10 @@ mod tests {
let url5 = "https://ethcore.io/parity.png";
- assert_eq!(test_guess_mime_type(url1), None);
- assert_eq!(test_guess_mime_type(url2), Some("image/png".into()));
- assert_eq!(test_guess_mime_type(url3), Some("image/png".into()));
- assert_eq!(test_guess_mime_type(url4), Some("image/jpeg".into()));
- assert_eq!(test_guess_mime_type(url5), Some("image/png".into()));
+ assert_eq!(guess_mime_type(url1), None);
+ assert_eq!(guess_mime_type(url2), Some("image/png".into()));
+ assert_eq!(guess_mime_type(url3), Some("image/png".into()));
+ assert_eq!(guess_mime_type(url4), Some("image/jpeg".into()));
+ assert_eq!(guess_mime_type(url5), Some("image/png".into()));
}
}
diff --git a/util/bigint/src/uint.rs b/util/bigint/src/uint.rs
index dab00537e..f4dd91140 100644
--- a/util/bigint/src/uint.rs
+++ b/util/bigint/src/uint.rs
@@ -683,6 +683,7 @@ macro_rules! construct_uint {
bytes[i] = (arr[pos] >> ((rev % 8) * 8)) as u8;
}
}
+
#[inline]
fn exp10(n: usize) -> Self {
match n {
diff --git a/util/fetch/src/lib.rs b/util/fetch/src/lib.rs
index 7ab38604b..8ec9e0ddd 100644
--- a/util/fetch/src/lib.rs
+++ b/util/fetch/src/lib.rs
@@ -26,4 +26,4 @@ extern crate rand;
pub mod client;
pub mod fetch_file;
-pub use self::client::{Client, Fetch, FetchError, FetchResult};
\ No newline at end of file
+pub use self::client::{Client, Fetch, FetchError, FetchResult};