diff --git a/contrib/CMakeLists.txt b/contrib/CMakeLists.txt index bd5ff43..908ce0e 100644 --- a/contrib/CMakeLists.txt +++ b/contrib/CMakeLists.txt @@ -22,6 +22,7 @@ set(SAPI_CONTRIB_SANDBOXES turbojpeg zopfli zstd + woff2 ) foreach(_contrib IN LISTS SAPI_CONTRIB_SANDBOXES) diff --git a/contrib/woff2/CMakeLists.txt b/contrib/woff2/CMakeLists.txt new file mode 100644 index 0000000..1472410 --- /dev/null +++ b/contrib/woff2/CMakeLists.txt @@ -0,0 +1,75 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# https://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. + +cmake_minimum_required(VERSION 3.13..3.22) +project(woff2-sapi CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +if(NOT TARGET sapi::sapi) + set(SAPI_ROOT "../.." CACHE PATH "Path to the Sandboxed API source tree") + add_subdirectory("${SAPI_ROOT}" + "${CMAKE_BINARY_DIR}/sandboxed-api-build" + # Omit this to have the full Sandboxed API in IDE + EXCLUDE_FROM_ALL) +endif() + +find_package(PkgConfig REQUIRED) +pkg_check_modules(WOFF2_ENC REQUIRED IMPORTED_TARGET GLOBAL libwoff2enc) +pkg_check_modules(WOFF2_DEC REQUIRED IMPORTED_TARGET GLOBAL libwoff2dec) +pkg_check_modules(WOFF2_COMMON REQUIRED IMPORTED_TARGET GLOBAL libwoff2common) + +add_library(woff2_sapi_wrapper woff2_wrapper.cc woff2_wrapper.h) +target_link_libraries(woff2_sapi_wrapper + PRIVATE + PkgConfig::WOFF2_ENC + PkgConfig::WOFF2_DEC + PkgConfig::WOFF2_COMMON +) + +add_sapi_library(woff2_sapi + FUNCTIONS + WOFF2_ConvertWOFF2ToTTF + WOFF2_ConvertTTFToWOFF2 + WOFF2_Free + INPUTS + "woff2_wrapper.h" + LIBRARY + woff2_sapi_wrapper + LIBRARY_NAME + WOFF2 + NAMESPACE + sapi_woff2 +) + +target_include_directories(woff2_sapi INTERFACE + "${PROJECT_BINARY_DIR}" + "${SAPI_SOURCE_DIR}" +) + +if(SAPI_ENABLE_TESTS) + enable_testing() + add_executable(woff2_sapi_test + woff2_sapi_test.cc + ) + target_link_libraries(woff2_sapi_test PRIVATE + woff2_sapi + sapi::base + gtest + gmock + ) + gtest_discover_tests(woff2_sapi_test PROPERTIES + ENVIRONMENT "TEST_DATA_DIR=${PROJECT_SOURCE_DIR}/testdata") +endif() diff --git a/contrib/woff2/testdata/Roboto-Regular.ttf b/contrib/woff2/testdata/Roboto-Regular.ttf new file mode 100644 index 0000000..54a284d Binary files /dev/null and b/contrib/woff2/testdata/Roboto-Regular.ttf differ diff --git a/contrib/woff2/testdata/Roboto-Regular.woff2 b/contrib/woff2/testdata/Roboto-Regular.woff2 new file mode 100644 index 0000000..9440ec8 Binary files /dev/null and b/contrib/woff2/testdata/Roboto-Regular.woff2 differ diff --git a/contrib/woff2/woff2_sapi.h b/contrib/woff2/woff2_sapi.h new file mode 100644 index 0000000..4f143c2 --- /dev/null +++ b/contrib/woff2/woff2_sapi.h @@ -0,0 +1,48 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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. + +#ifndef CONTRIB_WOFF2_WOFF2_SAPI_H_ +#define CONTRIB_WOFF2_WOFF2_SAPI_H_ + +#include + +#include + +#include "woff2_sapi.sapi.h" // NOLINT(build/include) +namespace sapi_woff2 { +class Woff2SapiSandbox : public WOFF2Sandbox { + public: + std::unique_ptr ModifyPolicy( + sandbox2::PolicyBuilder*) override { + return sandbox2::PolicyBuilder() + .AllowDynamicStartup() + .AllowSystemMalloc() + .AllowRead() + .AllowStat() + .AllowWrite() + .AllowExit() + .AllowSyscalls({ + __NR_futex, + __NR_close, + __NR_lseek, + __NR_getpid, + __NR_clock_gettime, + __NR_mmap, + __NR_madvise, + }) + .BuildOrDie(); + } +}; +} // namespace sapi_woff2 +#endif // CONTRIB_WOFF2_WOFF2_SAPI_H_ diff --git a/contrib/woff2/woff2_sapi_test.cc b/contrib/woff2/woff2_sapi_test.cc new file mode 100644 index 0000000..f5fcb2c --- /dev/null +++ b/contrib/woff2/woff2_sapi_test.cc @@ -0,0 +1,137 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// https://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. + +#include + +#include +#include +#include +#include +#include + +#include "contrib/woff2/woff2_sapi.h" +#include "gflags/gflags.h" +#include "glog/logging.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "sandboxed_api/testing.h" +#include "sandboxed_api/util/fileops.h" +#include "sandboxed_api/util/path.h" +#include "sandboxed_api/util/status_matchers.h" +#include "woff2_sapi.h" // NOLINT(build/include) +#include "woff2_sapi.sapi.h" // NOLINT(build/include) +#include "woff2_wrapper.h" // NOLINT(build/include) + +namespace { + +using ::sapi::IsOk; +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::Not; +using ::testing::StrEq; + +class Woff2SapiSandboxTest : public testing::Test { + protected: + static void SetUpTestSuite() { + test_data_dir_ = ::getenv("TEST_DATA_DIR"); + ASSERT_THAT(test_data_dir_, Not(IsNull())); + sandbox_ = new ::sapi_woff2::Woff2SapiSandbox(); + ASSERT_THAT(sandbox_->Init(), IsOk()); + api_ = new ::sapi_woff2::WOFF2Api(sandbox_); + } + static void TearDownTestSuite() { + delete api_; + delete sandbox_; + } + static absl::StatusOr> ReadFile( + const char* in_file, size_t expected_size = SIZE_MAX); + static const char* test_data_dir_; + static ::sapi_woff2::WOFF2Api* api_; + + private: + static ::sapi_woff2::Woff2SapiSandbox* sandbox_; +}; + +::sapi_woff2::Woff2SapiSandbox* Woff2SapiSandboxTest::sandbox_; +::sapi_woff2::WOFF2Api* Woff2SapiSandboxTest::api_; +const char* Woff2SapiSandboxTest::test_data_dir_; + +std::streamsize GetStreamSize(std::ifstream& stream) { + stream.seekg(0, std::ios_base::end); + std::streamsize ssize = stream.tellg(); + stream.seekg(0, std::ios_base::beg); + + return ssize; +} + +absl::StatusOr> Woff2SapiSandboxTest::ReadFile( + const char* in_file, size_t expected_size) { + auto env = absl::StrCat(test_data_dir_, "/", in_file); + std::ifstream f(env); + if (!f.is_open()) { + return absl::UnavailableError("File could not be opened"); + } + std::streamsize ssize = GetStreamSize(f); + if (expected_size != SIZE_MAX && ssize != expected_size) { + return absl::UnavailableError("Incorrect size of file"); + } + std::vector inbuf((ssize)); + f.read(reinterpret_cast(inbuf.data()), ssize); + if (ssize != f.gcount()) { + return absl::UnavailableError("Premature end of file"); + } + if (f.fail() || f.eof()) { + return absl::UnavailableError("Error reading file"); + } + return inbuf; +} + +TEST_F(Woff2SapiSandboxTest, Compress) { + auto result = ReadFile("Roboto-Regular.ttf"); + ASSERT_THAT(result, IsOk()); + sapi::v::Array array(result->data(), result->size()); + sapi::v::GenericPtr p; + sapi::v::IntBase out_length; + auto compress_result = api_->WOFF2_ConvertTTFToWOFF2( + array.PtrBefore(), result->size(), p.PtrAfter(), out_length.PtrAfter()); + ASSERT_THAT(compress_result, IsOk()); + ASSERT_TRUE(compress_result.value()); + ASSERT_THAT(p.GetValue(), Not(Eq(0))); + auto ptr = sapi::v::RemotePtr{reinterpret_cast(p.GetValue())}; + ASSERT_THAT(api_->WOFF2_Free(&ptr), IsOk()); +} + +TEST_F(Woff2SapiSandboxTest, Decompress) { + auto result = ReadFile("Roboto-Regular.woff2"); + ASSERT_THAT(result, IsOk()); + sapi::v::Array array(result->data(), result->size()); + sapi::v::GenericPtr p; + sapi::v::IntBase out_length; + auto decompress_result = api_->WOFF2_ConvertWOFF2ToTTF( + array.PtrBefore(), result->size(), p.PtrAfter(), out_length.PtrAfter(), + 1 << 25); + ASSERT_THAT(decompress_result, IsOk()); + ASSERT_TRUE(decompress_result.value()); + ASSERT_THAT(p.GetValue(), Not(Eq(0))); + auto ptr = sapi::v::RemotePtr{reinterpret_cast(p.GetValue())}; + ASSERT_THAT(api_->WOFF2_Free(&ptr), IsOk()); +} +} // namespace + +int main(int argc, char* argv[]) { + if (argc < 1) return 255; + ::google::InitGoogleLogging(argv[0]); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/contrib/woff2/woff2_wrapper.cc b/contrib/woff2/woff2_wrapper.cc new file mode 100644 index 0000000..15e64a3 --- /dev/null +++ b/contrib/woff2/woff2_wrapper.cc @@ -0,0 +1,42 @@ +#include +#include +#include + +#include +#include +#include + +extern "C" bool WOFF2_ConvertWOFF2ToTTF(const uint8_t* data, size_t length, + uint8_t** result, size_t* result_length, + size_t max_size) noexcept { + if (result) *result = nullptr; + if (result_length) *result_length = 0; + if (!data || !length || !result || !result_length) return false; + size_t final_size = ::woff2::ComputeWOFF2FinalSize(data, length); + if (final_size > (max_size ? max_size : woff2::kDefaultMaxSize)) return false; + std::unique_ptr buffer = std::make_unique(final_size); + woff2::WOFF2MemoryOut output(buffer.get(), final_size); + if (!::woff2::ConvertWOFF2ToTTF(data, length, &output)) return false; + *result = buffer.release(); + *result_length = final_size; + return true; +} + +extern "C" bool WOFF2_ConvertTTFToWOFF2(const uint8_t* data, size_t length, + uint8_t** result, + size_t* result_length) noexcept { + if (result) *result = nullptr; + if (result_length) *result_length = 0; + if (!data || !length || !result || !result_length) return false; + size_t size = woff2::MaxWOFF2CompressedSize(data, length); + std::unique_ptr buffer = std::make_unique(size); + if (!buffer) return false; + if (!woff2::ConvertTTFToWOFF2(data, length, buffer.get(), &size)) return false; + *result = buffer.release(); + *result_length = size; + return true; +} + +extern "C" void WOFF2_Free(uint8_t* data) noexcept { + std::unique_ptr p{data}; +} diff --git a/contrib/woff2/woff2_wrapper.h b/contrib/woff2/woff2_wrapper.h new file mode 100644 index 0000000..6b94375 --- /dev/null +++ b/contrib/woff2/woff2_wrapper.h @@ -0,0 +1,17 @@ +#ifndef CONTRIB_WOFF2_WOFF2_WRAPPER_H +#define CONTRIB_WOFF2_WOFF2_WRAPPER_H + +#include +#include +#include + +extern "C" { +bool WOFF2_ConvertWOFF2ToTTF(const uint8_t* data, size_t length, + uint8_t** result, size_t* result_length, + size_t max_size) noexcept; +bool WOFF2_ConvertTTFToWOFF2(const uint8_t* data, size_t length, + uint8_t** result, size_t* result_length) noexcept; +void WOFF2_Free(uint8_t* data) noexcept; +} + +#endif // CONTRIB_WOFF2_WOFF2_WRAPPER_H