Support versioning in CLI (#67)

This commit is contained in:
vms
2021-03-12 20:04:47 +03:00
committed by GitHub
parent b0f2738c94
commit 5effdcba72
58 changed files with 993 additions and 217 deletions

View File

@ -0,0 +1,59 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::ModuleInfoResult;
use crate::ModuleInfoError;
use walrus::IdsToIndices;
use walrus::Module;
use std::borrow::Cow;
pub(super) fn extract_custom_sections_by_name<'w>(
wasm_module: &'w Module,
section_name: &str,
) -> ModuleInfoResult<Vec<Cow<'w, [u8]>>> {
let default_ids = IdsToIndices::default();
let sections = wasm_module
.customs
.iter()
.filter(|(_, section)| section.name() == section_name)
.map(|s| s.1.data(&default_ids))
.collect::<Vec<_>>();
Ok(sections)
}
pub(super) fn try_as_one_section<'s>(
mut sections: Vec<Cow<'s, [u8]>>,
section_name: &'static str,
) -> ModuleInfoResult<Cow<'s, [u8]>> {
let sections_count = sections.len();
if sections_count > 1 {
return Err(ModuleInfoError::MultipleCustomSections(
section_name,
sections_count,
));
}
if sections_count == 0 {
return Err(ModuleInfoError::NoCustomSection(section_name));
}
Ok(sections.remove(0))
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use semver::SemVerError;
use thiserror::Error as ThisError;
use std::str::Utf8Error;
#[derive(Debug, ThisError)]
pub enum ModuleInfoError {
/// Version section is absent.
#[error("the module doesn't contain section with '{0}', probably it's compiled with an old sdk version")]
NoCustomSection(&'static str),
/// Multiple sections with the same name.
#[error("the module contains {1} sections with name '{0}' - it's corrupted")]
MultipleCustomSections(&'static str, usize),
/// Errors related to corrupted version.
#[error("{0}")]
VersionError(#[from] SDKVersionError),
/// Errors related to corrupted manifest.
#[error("{0}")]
ManifestError(#[from] ManifestError),
/// An error occurred while parsing Wasm file.
#[error("provided Wasm file is corrupted: {0}")]
CorruptedWasmFile(anyhow::Error),
}
#[derive(Debug, ThisError)]
pub enum SDKVersionError {
/// Version can't be parsed to Utf8 string.
#[error("embedded to the Wasm file version isn't valid UTF8 string: '{0}'")]
VersionNotValidUtf8(Utf8Error),
/// Version can't be parsed with semver.
#[error("embedded to the Wasm file version is corrupted: '{0}'")]
VersionCorrupted(#[from] SemVerError),
}
#[derive(Debug, ThisError, PartialEq)]
pub enum ManifestError {
/// Manifest of a Wasm file doesn't have enough bytes to read size of a field from its prefix.
#[error(
"{0} can't be read: embedded manifest doesn't contain enough bytes to read field size from prefix"
)]
NotEnoughBytesForPrefix(&'static str),
/// Manifest of a Wasm file doesn't have enough bytes to read a field.
#[error(
"{0} can't be read: embedded manifest doesn't contain enough bytes to read field of size {1}"
)]
NotEnoughBytesForField(&'static str, usize),
/// Manifest of a Wasm file doesn't have enough bytes to read field.
#[error("{0} is an invalid Utf8 string: {1}")]
FieldNotValidUtf8(&'static str, Utf8Error),
/// Size inside prefix of a field is too big (it exceeds usize or overflows with prefix size).
#[error("{0} has too big size: {1}")]
TooBigFieldSize(&'static str, u64),
/// Version can't be parsed with semver.
#[error("embedded to the Wasm file version is corrupted: '{0}'")]
ModuleVersionCorrupted(#[from] SemVerError),
/// Manifest contains some trailing characters.
#[error("embedded manifest is corrupted: there are some trailing characters")]
ManifestRemainderNotEmpty,
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#![warn(rust_2018_idioms)]
#![deny(
dead_code,
nonstandard_style,
unused_imports,
unused_mut,
unused_variables,
unused_unsafe,
unreachable_patterns
)]
mod custom_section_extractor;
mod errors;
mod manifest;
mod manifest_extractor;
mod version_extractor;
#[cfg(test)]
mod tests;
pub use errors::ModuleInfoError;
pub use errors::ManifestError;
pub use errors::SDKVersionError;
pub use version_extractor::extract_sdk_version_by_path;
pub use version_extractor::extract_sdk_version_by_module;
pub use manifest::ModuleManifest;
pub use manifest_extractor::extract_manifest_by_path;
pub use manifest_extractor::extract_version_by_module;
pub(crate) use custom_section_extractor::extract_custom_sections_by_name;
pub(crate) use custom_section_extractor::try_as_one_section;
pub(crate) type ModuleInfoResult<T> = std::result::Result<T, ModuleInfoError>;

View File

@ -0,0 +1,145 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// Describes manifest of a Wasm module in the Fluence network.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleManifest {
pub authors: String,
pub version: semver::Version,
pub description: String,
pub repository: String,
}
use crate::ManifestError;
use std::convert::TryFrom;
use std::str::FromStr;
type Result<T> = std::result::Result<T, ManifestError>;
impl TryFrom<&[u8]> for ModuleManifest {
type Error = ManifestError;
#[rustfmt::skip]
fn try_from(value: &[u8]) -> Result<Self> {
let (authors, next_offset) = try_extract_field_as_string(value, 0, "authors")?;
let (version, next_offset) = try_extract_field_as_version(value, next_offset, "version")?;
let (description, next_offset) = try_extract_field_as_string(value, next_offset, "description")?;
let (repository, next_offset) = try_extract_field_as_string(value, next_offset, "repository")?;
if next_offset != value.len() {
return Err(ManifestError::ManifestRemainderNotEmpty)
}
let manifest = ModuleManifest {
authors,
version,
description,
repository,
};
Ok(manifest)
}
}
fn try_extract_field_as_string(
raw_manifest: &[u8],
offset: usize,
field_name: &'static str,
) -> Result<(String, usize)> {
let raw_manifest = &raw_manifest[offset..];
let (field_as_bytes, read_len) = try_extract_prefixed_field(raw_manifest, field_name)?;
let field_as_string = try_to_str(field_as_bytes, field_name)?.to_string();
Ok((field_as_string, offset + read_len))
}
fn try_extract_field_as_version(
raw_manifest: &[u8],
offset: usize,
field_name: &'static str,
) -> Result<(semver::Version, usize)> {
let raw_manifest = &raw_manifest[offset..];
let (field_as_bytes, read_len) = try_extract_prefixed_field(raw_manifest, field_name)?;
let field_as_str = try_to_str(field_as_bytes, field_name)?;
let version = semver::Version::from_str(field_as_str)?;
Ok((version, offset + read_len))
}
const PREFIX_SIZE: usize = std::mem::size_of::<u64>();
fn try_extract_prefixed_field<'a>(
array: &'a [u8],
field_name: &'static str,
) -> Result<(&'a [u8], usize)> {
let field_len = try_extract_field_len(array, field_name)?;
let field = try_extract_field(array, field_len, field_name)?;
let read_size = PREFIX_SIZE + field.len();
Ok((field, read_size))
}
fn try_extract_field_len(array: &[u8], field_name: &'static str) -> Result<usize> {
if array.len() < PREFIX_SIZE {
return Err(ManifestError::NotEnoughBytesForPrefix(field_name));
}
let mut field_len = [0u8; PREFIX_SIZE];
field_len.copy_from_slice(&array[0..PREFIX_SIZE]);
let field_len = u64::from_le_bytes(field_len);
// TODO: Until we use Wasm32 and compiles our node to x86_64, converting to usize is sound
if field_len.checked_add(PREFIX_SIZE as u64).is_none()
|| usize::try_from(field_len + PREFIX_SIZE as u64).is_err()
{
return Err(ManifestError::TooBigFieldSize(field_name, field_len));
}
// it's safe to convert it to usize because it's been checked
Ok(field_len as usize)
}
fn try_extract_field<'a>(
array: &'a [u8],
field_len: usize,
field_name: &'static str,
) -> Result<&'a [u8]> {
if array.len() < PREFIX_SIZE + field_len {
return Err(ManifestError::NotEnoughBytesForField(field_name, field_len));
}
let field = &array[PREFIX_SIZE..PREFIX_SIZE + field_len];
Ok(field)
}
fn try_to_str<'v>(value: &'v [u8], field_name: &'static str) -> Result<&'v str> {
match std::str::from_utf8(value) {
Ok(s) => Ok(s),
Err(e) => Err(ManifestError::FieldNotValidUtf8(field_name, e)),
}
}
use std::fmt;
impl fmt::Display for ModuleManifest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "authors: {}", self.authors)?;
writeln!(f, "version: {}", self.version)?;
writeln!(f, "description: {}", self.description)?;
write!(f, "repository: {}", self.repository)
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::ModuleInfoResult;
use crate::ModuleInfoError;
use crate::extract_custom_sections_by_name;
use crate::try_as_one_section;
use crate::ModuleManifest;
use fluence_sdk_main::MANIFEST_SECTION_NAME;
use walrus::ModuleConfig;
use walrus::Module;
use std::borrow::Cow;
use std::path::Path;
use std::convert::TryInto;
pub fn extract_manifest_by_path(
wasm_module_path: &Path,
) -> ModuleInfoResult<Option<ModuleManifest>> {
let module = ModuleConfig::new()
.parse_file(wasm_module_path)
.map_err(ModuleInfoError::CorruptedWasmFile)?;
extract_version_by_module(&module)
}
pub fn extract_version_by_module(wasm_module: &Module) -> ModuleInfoResult<Option<ModuleManifest>> {
let sections = extract_custom_sections_by_name(&wasm_module, MANIFEST_SECTION_NAME)?;
if sections.is_empty() {
return Ok(None);
}
let section = try_as_one_section(sections, MANIFEST_SECTION_NAME)?;
let manifest = match section {
Cow::Borrowed(bytes) => bytes.try_into(),
Cow::Owned(vec) => vec.as_slice().try_into(),
}?;
Ok(Some(manifest))
}

View File

@ -0,0 +1,154 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::ManifestError;
use crate::ModuleManifest;
use std::convert::TryInto;
use std::str::FromStr;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ByteEncoder {
buffer: Vec<u8>,
}
impl ByteEncoder {
pub fn new() -> Self {
<_>::default()
}
pub fn add_u64(&mut self, number: u64) {
use std::io::Write;
let number_le_bytes = number.to_le_bytes();
self.buffer
.write(&number_le_bytes)
.expect("writing to buffer should be successful");
}
pub fn add_utf8_string(&mut self, str: &str) {
use std::io::Write;
let str_as_bytes = str.as_bytes();
self.buffer
.write(&str_as_bytes)
.expect("writing to buffer should be successful");
}
pub fn add_utf8_field(&mut self, field: &str) {
let field_len = field.as_bytes().len();
self.add_u64(field_len as u64);
self.add_utf8_string(field);
}
pub fn as_bytes(&self) -> &[u8] {
&self.buffer
}
#[allow(dead_code)]
pub fn into_vec(self) -> Vec<u8> {
self.buffer
}
}
#[test]
fn test_reading_simple_config() {
let authors = "authors".to_string();
let version = semver::Version::from_str("0.1.0").unwrap();
let description = "description".to_string();
let repository = "repository".to_string();
let mut array = ByteEncoder::new();
array.add_utf8_field(&authors);
array.add_utf8_field(&version.to_string());
array.add_utf8_field(&description);
array.add_utf8_field(&repository);
let actual: ModuleManifest = array
.as_bytes()
.try_into()
.expect("module manifest should be deserialized correctly");
let expected = ModuleManifest {
authors,
version,
description,
repository,
};
assert_eq!(actual, expected);
}
#[test]
fn test_too_big_field_len() {
let mut array = ByteEncoder::new();
array.add_utf8_field("authors");
let incorrect_size = u64::MAX;
array.add_u64(incorrect_size);
array.add_utf8_string("version");
array.add_utf8_field("description");
array.add_utf8_field("repository");
let actual: Result<ModuleManifest, _> = array.as_bytes().try_into();
let expected = Err(ManifestError::TooBigFieldSize("version", incorrect_size));
assert_eq!(actual, expected);
}
#[test]
fn test_without_one_field() {
let mut array = ByteEncoder::new();
array.add_utf8_field("authors");
array.add_utf8_field("0.1.0");
array.add_utf8_field("description");
let actual: Result<ModuleManifest, _> = array.as_bytes().try_into();
let expected = Err(ManifestError::NotEnoughBytesForPrefix("repository"));
assert_eq!(actual, expected);
}
#[test]
fn test_with_empty_slice() {
let actual: Result<ModuleManifest, _> = vec![].as_slice().try_into();
let expected = Err(ManifestError::NotEnoughBytesForPrefix("authors"));
assert_eq!(actual, expected);
}
#[test]
fn test_not_enough_data_for_field() {
let mut array = ByteEncoder::new();
array.add_utf8_field("authors");
array.add_utf8_field("0.1.0");
array.add_utf8_field("description");
let too_big_size = 0xFF;
array.add_u64(too_big_size);
array.add_utf8_string("repository");
let actual: Result<ModuleManifest, _> = array.as_bytes().try_into();
let expected = Err(ManifestError::NotEnoughBytesForField(
"repository",
too_big_size as usize,
));
assert_eq!(actual, expected);
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::ModuleInfoResult;
use crate::ModuleInfoError;
use crate::SDKVersionError;
use crate::extract_custom_sections_by_name;
use crate::try_as_one_section;
use fluence_sdk_main::VERSION_SECTION_NAME;
use walrus::ModuleConfig;
use walrus::Module;
use std::borrow::Cow;
use std::str::FromStr;
use std::path::Path;
pub fn extract_sdk_version_by_path(
wasm_module_path: &Path,
) -> ModuleInfoResult<Option<semver::Version>> {
let module = ModuleConfig::new()
.parse_file(wasm_module_path)
.map_err(ModuleInfoError::CorruptedWasmFile)?;
extract_sdk_version_by_module(&module)
}
pub fn extract_sdk_version_by_module(
wasm_module: &Module,
) -> ModuleInfoResult<Option<semver::Version>> {
let sections = extract_custom_sections_by_name(&wasm_module, VERSION_SECTION_NAME)?;
if sections.is_empty() {
return Ok(None);
}
let section = try_as_one_section(sections, VERSION_SECTION_NAME)?;
let version = match section {
Cow::Borrowed(bytes) => as_semver(bytes),
Cow::Owned(vec) => as_semver(&vec),
}?;
Ok(Some(version))
}
fn as_semver(version_as_bytes: &[u8]) -> Result<semver::Version, crate::SDKVersionError> {
match std::str::from_utf8(version_as_bytes) {
Ok(str) => Ok(semver::Version::from_str(str)?),
Err(e) => Err(SDKVersionError::VersionNotValidUtf8(e)),
}
}