Integrate libfdt and add rust wrappers
Booting Android/Linux requires to pass additional platform specific
information such as ramdisk load address range and bootargs via FDT.
Port and integrate third party libfdt C library for this task.
Bug: 305093905
Change-Id: I2fdc6665795674188aefd45e02658817681cba7c
diff --git a/gbl/efi/BUILD b/gbl/efi/BUILD
index 034cc40..a17be8e 100644
--- a/gbl/efi/BUILD
+++ b/gbl/efi/BUILD
@@ -25,6 +25,7 @@
srcs = [
"src/main.rs",
"src/riscv64.rs",
+ "src/utils.rs",
],
linker_script = select({
"@gbl//toolchain:gbl_rust_elf_riscv64": "@gbl//efi/arch/riscv64:riscv64_efi.lds",
@@ -36,6 +37,7 @@
],
deps = [
"@gbl//libefi",
+ "@gbl//libfdt",
"@gbl//libstorage",
"@gbl//third_party/libzbi",
] + select({
diff --git a/gbl/efi/src/main.rs b/gbl/efi/src/main.rs
index 6a0c569..1523bba 100644
--- a/gbl/efi/src/main.rs
+++ b/gbl/efi/src/main.rs
@@ -18,87 +18,24 @@
#[cfg(target_arch = "riscv64")]
mod riscv64;
+use core::ffi::CStr;
use core::fmt::Write;
-use gbl_storage::{required_scratch_size, BlockDevice, Gpt, StorageError};
+use core::str::from_utf8;
+use gbl_storage::{required_scratch_size, BlockDevice, Gpt};
use efi::defs::EfiSystemTable;
-use efi::{
- initialize, BlockIoProtocol, DeviceHandle, DevicePathProtocol, DevicePathText,
- DevicePathToTextProtocol, EfiEntry, EfiError, LoadedImageProtocol, Protocol,
-};
+use efi::{initialize, BlockIoProtocol, LoadedImageProtocol};
+use fdt::Fdt;
// For the `vec!` macro
#[macro_use]
extern crate alloc;
-// Implement a block device on top of BlockIoProtocol
-struct EfiBlockDevice<'a>(Protocol<'a, BlockIoProtocol>);
+#[macro_use]
+mod utils;
+use utils::{get_device_path, get_efi_fdt, EfiBlockDevice, Result};
-impl BlockDevice for EfiBlockDevice<'_> {
- fn block_size(&mut self) -> u64 {
- self.0.media().unwrap().block_size as u64
- }
-
- fn num_blocks(&mut self) -> u64 {
- (self.0.media().unwrap().last_block + 1) as u64
- }
-
- fn alignment(&mut self) -> u64 {
- core::cmp::max(1, self.0.media().unwrap().io_align as u64)
- }
-
- fn read_blocks(&mut self, blk_offset: u64, out: &mut [u8]) -> bool {
- self.0.read_blocks(blk_offset, out).is_ok()
- }
-
- fn write_blocks(&mut self, blk_offset: u64, data: &[u8]) -> bool {
- self.0.write_blocks(blk_offset, data).is_ok()
- }
-}
-
-/// A top level error type that consolidates errors from different libraries.
-#[derive(Debug)]
-enum GblEfiError {
- StorageError(gbl_storage::StorageError),
- EfiError(efi::EfiError),
-}
-
-impl From<StorageError> for GblEfiError {
- fn from(error: StorageError) -> GblEfiError {
- GblEfiError::StorageError(error)
- }
-}
-
-impl From<EfiError> for GblEfiError {
- fn from(error: EfiError) -> GblEfiError {
- GblEfiError::EfiError(error)
- }
-}
-
-/// Helper function to get the `DevicePathText` from a `DeviceHandle`.
-fn get_device_path<'a>(
- entry: &'a EfiEntry,
- handle: DeviceHandle,
-) -> Result<DevicePathText<'a>, GblEfiError> {
- let bs = entry.system_table().boot_services();
- let path = bs.open_protocol::<DevicePathProtocol>(handle)?;
- let path_to_text = bs.find_first_and_open::<DevicePathToTextProtocol>()?;
- Ok(path_to_text.convert_device_path_to_text(&path, false, false)?)
-}
-
-/// Helper macro for printing message via `EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL` in
-/// `EFI_SYSTEM_TABLE.ConOut`.
-#[macro_export]
-macro_rules! efi_print {
- ( $efi_entry:expr, $( $x:expr ),* ) => {
- write!($efi_entry.system_table().con_out().unwrap(), $($x,)*).unwrap()
- };
-}
-
-fn main(
- image_handle: *mut core::ffi::c_void,
- systab_ptr: *mut EfiSystemTable,
-) -> Result<(), GblEfiError> {
+fn main(image_handle: *mut core::ffi::c_void, systab_ptr: *mut EfiSystemTable) -> Result<()> {
let entry = initialize(image_handle, systab_ptr);
efi_print!(entry, "\n\n****Rust EFI Application****\n\n");
@@ -137,6 +74,28 @@
}
}
}
+
+ // Check if we have a device tree.
+ match get_efi_fdt(&entry) {
+ Some((_, bytes)) => {
+ let fdt = Fdt::new(bytes)?;
+ efi_print!(entry, "Device tree found\n");
+ let print_property = |node: &str, name: &CStr| -> Result<()> {
+ efi_print!(
+ entry,
+ "{}:{} = {}\n",
+ node,
+ name.to_str().unwrap(),
+ from_utf8(fdt.get_property(node, name).unwrap_or("NA".as_bytes())).unwrap()
+ );
+ Ok(())
+ };
+ print_property("/", CStr::from_bytes_with_nul(b"compatible\0").unwrap())?;
+ print_property("/chosen", CStr::from_bytes_with_nul(b"u-boot,version\0").unwrap())?;
+ print_property("/chosen", CStr::from_bytes_with_nul(b"bootargs\0").unwrap())?;
+ }
+ _ => {}
+ }
Ok(())
}
@@ -145,3 +104,6 @@
main(image_handle, systab_ptr).unwrap();
loop {}
}
+
+#[no_mangle]
+pub extern "C" fn __chkstk() {}
diff --git a/gbl/efi/src/utils.rs b/gbl/efi/src/utils.rs
new file mode 100644
index 0000000..8e5494f
--- /dev/null
+++ b/gbl/efi/src/utils.rs
@@ -0,0 +1,111 @@
+// Copyright 2023, The Android Open Source Project
+//
+// 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 efi::defs::EfiGuid;
+use efi::{
+ BlockIoProtocol, DeviceHandle, DevicePathProtocol, DevicePathText, DevicePathToTextProtocol,
+ EfiEntry, EfiError, Protocol,
+};
+use fdt::{FdtError, FdtHeader};
+use gbl_storage::{BlockDevice, StorageError};
+
+pub const EFI_DTB_TABLE_GUID: EfiGuid =
+ EfiGuid::new(0xb1b621d5, 0xf19c, 0x41a5, [0x83, 0x0b, 0xd9, 0x15, 0x2c, 0x69, 0xaa, 0xe0]);
+
+/// Helper macro for printing message via `EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL` in
+/// `EFI_SYSTEM_TABLE.ConOut`.
+#[macro_export]
+macro_rules! efi_print {
+ ( $efi_entry:expr, $( $x:expr ),* ) => {
+ write!($efi_entry.system_table().con_out().unwrap(), $($x,)*).unwrap()
+ };
+}
+
+/// GBL EFI application error type.
+pub type Result<T> = core::result::Result<T, GblEfiError>;
+
+/// A top level error type that consolidates errors from different libraries.
+#[derive(Debug)]
+pub enum GblEfiError {
+ StorageError(gbl_storage::StorageError),
+ EfiError(efi::EfiError),
+ FdtError(fdt::FdtError),
+}
+
+impl From<StorageError> for GblEfiError {
+ fn from(error: StorageError) -> GblEfiError {
+ GblEfiError::StorageError(error)
+ }
+}
+
+impl From<EfiError> for GblEfiError {
+ fn from(error: EfiError) -> GblEfiError {
+ GblEfiError::EfiError(error)
+ }
+}
+
+impl From<FdtError> for GblEfiError {
+ fn from(error: FdtError) -> GblEfiError {
+ GblEfiError::FdtError(error)
+ }
+}
+
+// Implement a block device on top of BlockIoProtocol
+pub struct EfiBlockDevice<'a>(pub Protocol<'a, BlockIoProtocol>);
+
+impl BlockDevice for EfiBlockDevice<'_> {
+ fn block_size(&mut self) -> u64 {
+ self.0.media().unwrap().block_size as u64
+ }
+
+ fn num_blocks(&mut self) -> u64 {
+ (self.0.media().unwrap().last_block + 1) as u64
+ }
+
+ fn alignment(&mut self) -> u64 {
+ core::cmp::max(1, self.0.media().unwrap().io_align as u64)
+ }
+
+ fn read_blocks(&mut self, blk_offset: u64, out: &mut [u8]) -> bool {
+ self.0.read_blocks(blk_offset, out).is_ok()
+ }
+
+ fn write_blocks(&mut self, blk_offset: u64, data: &[u8]) -> bool {
+ self.0.write_blocks(blk_offset, data).is_ok()
+ }
+}
+
+/// Helper function to get the `DevicePathText` from a `DeviceHandle`.
+pub fn get_device_path<'a>(
+ entry: &'a EfiEntry,
+ handle: DeviceHandle,
+) -> Result<DevicePathText<'a>> {
+ let bs = entry.system_table().boot_services();
+ let path = bs.open_protocol::<DevicePathProtocol>(handle)?;
+ let path_to_text = bs.find_first_and_open::<DevicePathToTextProtocol>()?;
+ Ok(path_to_text.convert_device_path_to_text(&path, false, false)?)
+}
+
+/// Find FDT from EFI configuration table.
+pub fn get_efi_fdt<'a>(entry: &'a EfiEntry) -> Option<(&FdtHeader, &[u8])> {
+ if let Some(config_tables) = entry.system_table().configuration_table() {
+ for table in config_tables {
+ if table.vendor_guid == EFI_DTB_TABLE_GUID {
+ // SAFETY: Buffer provided by EFI configuration table.
+ return unsafe { FdtHeader::from_raw(table.vendor_table as *const _).ok() };
+ }
+ }
+ }
+ None
+}
diff --git a/gbl/integration/aosp_u-boot-mainline/workspace.bzl b/gbl/integration/aosp_u-boot-mainline/workspace.bzl
index 425e76c..57ee8e9 100644
--- a/gbl/integration/aosp_u-boot-mainline/workspace.bzl
+++ b/gbl/integration/aosp_u-boot-mainline/workspace.bzl
@@ -81,6 +81,12 @@
""",
)
+ native.new_local_repository(
+ name = "libfdt_c",
+ path = "external/dtc/libfdt",
+ build_file = "@gbl//libfdt:BUILD.libfdt_c.bazel",
+ )
+
# Following are third party rust crates dependencies.
THIRD_PARTY_CRATES = [
diff --git a/gbl/libc/BUILD b/gbl/libc/BUILD
new file mode 100644
index 0000000..d2d7d23
--- /dev/null
+++ b/gbl/libc/BUILD
@@ -0,0 +1,34 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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.
+
+load("@rules_rust//rust:defs.bzl", "rust_library")
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+rust_library(
+ name = "libc",
+ srcs = glob(["**/*.rs"]),
+ edition = "2021",
+)
+
+cc_library(
+ name = "headers",
+ hdrs = [
+ "include/stdlib.h",
+ "include/string.h",
+ ],
+ includes = ["include"],
+)
diff --git a/gbl/libc/include/stdlib.h b/gbl/libc/include/stdlib.h
new file mode 100644
index 0000000..f9e0726
--- /dev/null
+++ b/gbl/libc/include/stdlib.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#ifndef __STDLIB_H__
+#define __STDLIB_H__
+
+int memcmp(const void* ptr1, const void* ptr2, size_t num);
+void* memmove(void* destination, const void* source, size_t num);
+
+#endif
diff --git a/gbl/libc/include/string.h b/gbl/libc/include/string.h
new file mode 100644
index 0000000..bee1457
--- /dev/null
+++ b/gbl/libc/include/string.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#ifndef __STDLIB_STRING_H__
+#define __STDLIB_STRING_H__
+
+#include <stddef.h>
+
+size_t strlen(const char * str);
+void *memchr(const void *ptr, int ch, size_t count);
+char *strrchr(const char *str, int c);
+size_t strnlen(const char *s, size_t maxlen);
+
+#endif
diff --git a/gbl/libc/src/lib.rs b/gbl/libc/src/lib.rs
new file mode 100644
index 0000000..ab19c49
--- /dev/null
+++ b/gbl/libc/src/lib.rs
@@ -0,0 +1,69 @@
+// Copyright 2023, The Android Open Source Project
+//
+// 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.
+
+//! This library provides implementation for a few libc functions for building third party C
+//! libraries.
+
+#![no_std]
+
+use core::ptr::null_mut;
+use core::slice::from_raw_parts;
+
+/// void *memchr(const void *ptr, int ch, size_t count);
+#[no_mangle]
+pub extern "C" fn memchr(
+ ptr: *const core::ffi::c_void,
+ ch: core::ffi::c_int,
+ count: core::ffi::c_ulong,
+) -> *mut core::ffi::c_void {
+ assert!(!ptr.is_null());
+ let start = ptr as *const u8;
+ let target = (ch & 0xff) as u8;
+ for i in 0..count {
+ // SAFETY: Buffer is assumed valid and bounded by count.
+ let curr = unsafe { start.add(i.try_into().unwrap()) };
+ // SAFETY: Buffer is assumed valid and bounded by count.
+ if *unsafe { curr.as_ref().unwrap() } == target {
+ return curr as *mut _;
+ }
+ }
+ null_mut()
+}
+
+/// char *strrchr(const char *str, int c);
+#[no_mangle]
+pub extern "C" fn strrchr(
+ ptr: *const core::ffi::c_char,
+ ch: core::ffi::c_int,
+) -> *mut core::ffi::c_char {
+ assert!(!ptr.is_null());
+ // SAFETY: Input is a valid null terminated string.
+ let bytes = unsafe { core::ffi::CStr::from_ptr(ptr).to_bytes_with_nul() };
+ let target = (ch & 0xff) as u8;
+ for c in bytes.iter().rev() {
+ if *c == target {
+ return c as *const _ as *mut _;
+ }
+ }
+ null_mut()
+}
+
+/// size_t strnlen(const char *s, size_t maxlen);
+#[no_mangle]
+pub fn strnlen(s: *const core::ffi::c_char, maxlen: usize) -> usize {
+ match memchr(s as *const _, 0, maxlen.try_into().unwrap()) {
+ p if p.is_null() => maxlen,
+ p => (p as usize) - (s as usize),
+ }
+}
diff --git a/gbl/libefi/src/lib.rs b/gbl/libefi/src/lib.rs
index baac2f3..39d6014 100644
--- a/gbl/libefi/src/lib.rs
+++ b/gbl/libefi/src/lib.rs
@@ -51,6 +51,7 @@
#![cfg_attr(not(test), no_std)]
use core::ptr::null_mut;
+use core::slice::from_raw_parts;
#[cfg(not(test))]
use core::{fmt::Write, panic::PanicInfo};
@@ -150,7 +151,6 @@
/// `SystemTable` provides methods for accessing fields in `EFI_SYSTEM_TABLE`.
#[derive(Clone, Copy)]
-//pub struct SystemTable<'a>(&'a EfiEntry);
pub struct SystemTable<'a> {
efi_entry: &'a EfiEntry,
table: &'a EfiSystemTable,
@@ -174,6 +174,20 @@
self.efi_entry,
))
}
+
+ /// Get the `EFI_SYSTEM_TABLE.ConfigurationTable` array.
+ pub fn configuration_table(&self) -> Option<&[EfiConfigurationTable]> {
+ match self.table.configuration_table.is_null() {
+ true => None,
+ // SAFETY: Non-null pointer to EFI configuration table.
+ false => unsafe {
+ Some(from_raw_parts(
+ self.table.configuration_table,
+ self.table.number_of_table_entries,
+ ))
+ },
+ }
+ }
}
/// `BootServices` provides methods for accessing various EFI_BOOT_SERVICES interfaces.
@@ -295,7 +309,7 @@
Self {
// SAFETY: Given correct UEFI firmware, non-null pointer points to valid memory.
// The memory is owned by the objects.
- handles: unsafe { core::slice::from_raw_parts(handles as *mut DeviceHandle, len) },
+ handles: unsafe { from_raw_parts(handles as *mut DeviceHandle, len) },
efi_entry: efi_entry,
}
}
diff --git a/gbl/libfdt/BUILD b/gbl/libfdt/BUILD
new file mode 100644
index 0000000..6b833c4
--- /dev/null
+++ b/gbl/libfdt/BUILD
@@ -0,0 +1,90 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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.
+
+load("@gbl//toolchain:gbl_toolchain.bzl", "link_static_cc_library")
+load("@gbl_llvm_prebuilts//:info.bzl", "LLVM_PREBUILTS_C_INCLUDE")
+load("@rules_rust//bindgen:bindgen.bzl", "rust_bindgen")
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+rust_bindgen(
+ name = "libfdt_c_bindgen",
+ bindgen_flags = [
+ "--ctypes-prefix",
+ "core::ffi",
+ "--use-core",
+ "--with-derive-custom-struct=fdt_header=AsBytes,FromBytes,FromZeroes,PartialEq",
+ "--allowlist-function",
+ "(fdt_.*)",
+ "--allowlist-type",
+ "(fdt_.*)",
+ "--raw-line",
+ """
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(dead_code)]
+#![cfg_attr(not(test), no_std)]
+
+use zerocopy::{AsBytes, FromBytes, FromZeroes};
+""",
+ ],
+ cc_lib = "@libfdt_c",
+ # For x86_32, we need to explicitly specify 32bit architecture.
+ clang_flags = select({
+ "@gbl//toolchain:gbl_rust_uefi_x86_32": ["-m32"],
+ "//conditions:default": ["-m64"],
+ }) + ["-I{}".format(LLVM_PREBUILTS_C_INCLUDE)],
+ header = "@libfdt_c//:libfdt.h",
+)
+
+genrule(
+ name = "bindgen_source",
+ srcs = [":libfdt_c_bindgen"],
+ outs = ["src/libfdt_c_Def.rs"],
+ cmd = "cp $(SRCS) $(OUTS)",
+)
+
+rust_library(
+ name = "libfdt_c_def",
+ srcs = [":bindgen_source"],
+ crate_root = ":bindgen_source",
+ deps = ["@zerocopy"],
+)
+
+rust_library(
+ name = "libfdt",
+ srcs = ["src/lib.rs"],
+ crate_name = "fdt",
+ edition = "2021",
+ deps = [
+ ":libfdt_c_def",
+ ":libfdt_c_static",
+ "@gbl//libc",
+ "@zerocopy",
+ ],
+)
+
+rust_test(
+ name = "libfdt_test",
+ compile_data = ["@gbl//libfdt/test:test.dtb"],
+ crate = ":libfdt",
+)
+
+link_static_cc_library(
+ name = "libfdt_c_static",
+ cc_library = "@libfdt_c",
+)
diff --git a/gbl/libfdt/BUILD.libfdt_c.bazel b/gbl/libfdt/BUILD.libfdt_c.bazel
new file mode 100644
index 0000000..6a8fed4
--- /dev/null
+++ b/gbl/libfdt/BUILD.libfdt_c.bazel
@@ -0,0 +1,50 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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.
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+exports_files(glob(["**/*"]))
+
+cc_library(
+ name = "libfdt_c",
+ srcs = [
+ "acpi.c",
+ "fdt.c",
+ "fdt_addresses.c",
+ "fdt_check.c",
+ "fdt_empty_tree.c",
+ "fdt_overlay.c",
+ "fdt_ro.c",
+ "fdt_rw.c",
+ "fdt_strerror.c",
+ "fdt_sw.c",
+ "fdt_wip.c",
+ ],
+ hdrs = [
+ "fdt.h",
+ "libacpi.h",
+ "libfdt.h",
+ "libfdt_env.h",
+ "libfdt_internal.h",
+ ],
+ copts = [
+ "-Wno-int-conversion",
+ "-Wno-implicit-function-declaration",
+ "-Wno-pointer-integer-compare",
+ ],
+ includes = ["."],
+ deps = ["@gbl//libc:headers"],
+)
diff --git a/gbl/libfdt/src/lib.rs b/gbl/libfdt/src/lib.rs
new file mode 100644
index 0000000..da424f9
--- /dev/null
+++ b/gbl/libfdt/src/lib.rs
@@ -0,0 +1,394 @@
+// Copyright 2023, The Android Open Source Project
+//
+// 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.
+
+//! This library provides a few wrapper APIs for libfdt_c
+
+#![cfg_attr(not(test), no_std)]
+
+extern crate libc;
+
+use core::ffi::CStr;
+use core::mem::size_of;
+use core::slice::{from_raw_parts, from_raw_parts_mut};
+
+use libfdt_c_def::{
+ fdt_add_subnode_namelen, fdt_header, fdt_setprop, fdt_setprop_placeholder, fdt_strerror,
+ fdt_subnode_offset_namelen,
+};
+
+use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref};
+
+/// libfdt error type.
+#[derive(Debug)]
+pub enum FdtError {
+ CLibError(&'static str),
+ FdtBytesNotMutable,
+ InvalidInput,
+ IntegerOverflow,
+}
+
+/// libfdt result type,
+pub type Result<T> = core::result::Result<T, FdtError>;
+
+/// Convert libfdt_c error code to Result
+fn map_result(code: core::ffi::c_int) -> Result<core::ffi::c_int> {
+ match code {
+ // SAFETY: Static null terminated string returned from libfdt_c API.
+ v if v < 0 => Err(FdtError::CLibError(unsafe {
+ core::ffi::CStr::from_ptr(fdt_strerror(v)).to_str().unwrap()
+ })),
+ v => Ok(v),
+ }
+}
+
+/// Check header and verified that totalsize does not exceed buffer size.
+fn fdt_check_header(fdt: &[u8]) -> Result<()> {
+ map_result(unsafe { libfdt_c_def::fdt_check_header(fdt.as_ptr() as *const _) })?;
+ match FdtHeader::from_bytes_ref(fdt)?.totalsize() <= fdt.len() {
+ true => Ok(()),
+ _ => Err(FdtError::InvalidInput),
+ }
+}
+
+/// Wrapper of fdt_add_subnode_namelen()
+fn fdt_add_subnode(
+ fdt: &mut [u8],
+ parent: core::ffi::c_int,
+ name: &str,
+) -> Result<core::ffi::c_int> {
+ // SAFETY: API from libfdt_c.
+ map_result(unsafe {
+ fdt_add_subnode_namelen(
+ fdt.as_mut_ptr() as *mut _,
+ parent,
+ name.as_ptr() as *const _,
+ name.len().try_into().map_err(|_| FdtError::IntegerOverflow)?,
+ )
+ })
+}
+
+/// Wrapper of fdt_subnode_offset_namelen()
+fn fdt_subnode_offset(
+ fdt: &[u8],
+ parent: core::ffi::c_int,
+ name: &str,
+) -> Result<core::ffi::c_int> {
+ // SAFETY: API from libfdt_c.
+ map_result(unsafe {
+ fdt_subnode_offset_namelen(
+ fdt.as_ptr() as *const _,
+ parent,
+ name.as_ptr() as *const _,
+ name.len().try_into().map_err(|_| FdtError::IntegerOverflow)?,
+ )
+ })
+}
+
+#[repr(transparent)]
+#[derive(Debug, Copy, Clone, AsBytes, FromBytes, FromZeroes, PartialEq)]
+pub struct FdtHeader(fdt_header);
+
+impl FdtHeader {
+ /// Return the totalsize field.
+ pub fn totalsize(&self) -> usize {
+ u32::from_be(self.0.totalsize) as usize
+ }
+
+ /// Return the minimal size of the FDT. Disregard trailing free space.
+ pub fn actual_size(&self) -> usize {
+ u32::from_be(self.0.off_dt_strings)
+ .checked_add(u32::from_be(self.0.size_dt_strings))
+ .unwrap() as usize
+ }
+
+ /// Update the totalsize field.
+ pub fn set_totalsize(&mut self, value: u32) {
+ self.0.totalsize = value.to_be();
+ }
+
+ /// Cast a bytes into a reference of FDT header
+ pub fn from_bytes_ref(buffer: &[u8]) -> Result<&FdtHeader> {
+ Ok(Ref::<_, FdtHeader>::new_from_prefix(buffer)
+ .ok_or_else(|| FdtError::InvalidInput)?
+ .0
+ .into_ref())
+ }
+
+ /// Cast a bytes into a mutable reference of FDT header.
+ pub fn from_bytes_mut(buffer: &mut [u8]) -> Result<&mut FdtHeader> {
+ Ok(Ref::<_, FdtHeader>::new_from_prefix(buffer)
+ .ok_or_else(|| FdtError::InvalidInput)?
+ .0
+ .into_mut())
+ }
+
+ /// Get FDT header and raw bytes from a raw pointer.
+ ///
+ /// Caller should guarantee that
+ /// 1. `ptr` contains a valid FDT.
+ /// 2. The buffer remains valid as long as the returned references are in use.
+ pub unsafe fn from_raw(ptr: *const u8) -> Result<(&'static FdtHeader, &'static [u8])> {
+ map_result(unsafe { libfdt_c_def::fdt_check_header(ptr as *const _) })?;
+ let header_bytes = from_raw_parts(ptr, size_of::<FdtHeader>());
+ let header = Self::from_bytes_ref(header_bytes)?;
+ Ok((header, from_raw_parts(ptr, header.totalsize())))
+ }
+}
+
+/// Object for managing an FDT.
+pub struct Fdt<T>(T);
+
+/// Read only APIs.
+impl<T: AsRef<[u8]>> Fdt<T> {
+ pub fn new(init: T) -> Result<Self> {
+ fdt_check_header(init.as_ref())?;
+ Ok(Fdt(init))
+ }
+
+ pub fn header_ref(&self) -> Result<&FdtHeader> {
+ FdtHeader::from_bytes_ref(self.0.as_ref())
+ }
+
+ /// Returns the totalsize according to FDT header. Trailing free space is included.
+ pub fn size(&self) -> Result<usize> {
+ Ok(self.header_ref()?.totalsize())
+ }
+
+ /// Get a property from an existing node.
+ pub fn get_property<'a>(&'a self, path: &str, name: &CStr) -> Result<&'a [u8]> {
+ let node = self.find_node(path)?;
+ let mut len: core::ffi::c_int = 0;
+ // SAFETY: API from libfdt_c.
+ let ptr = unsafe {
+ libfdt_c_def::fdt_get_property(
+ self.0.as_ref().as_ptr() as *const _,
+ node,
+ name.to_bytes_with_nul().as_ptr() as *const _,
+ &mut len as *mut _,
+ )
+ };
+ // SAFETY: Buffer returned by API from libfdt_c.
+ match unsafe { ptr.as_ref() } {
+ // SAFETY: Buffer returned by API from libfdt_c.
+ Some(v) => Ok(unsafe {
+ from_raw_parts(
+ v.data.as_ptr() as *const u8,
+ u32::from_be(v.len).try_into().map_err(|_| FdtError::IntegerOverflow)?,
+ )
+ }),
+ _ => Err(map_result(len).unwrap_err()),
+ }
+ }
+
+ /// Find the offset of a node by a given node path.
+ fn find_node(&self, path: &str) -> Result<core::ffi::c_int> {
+ let mut curr: core::ffi::c_int = 0;
+ for name in path.split('/') {
+ if name.len() == 0 {
+ continue;
+ }
+ curr = fdt_subnode_offset(self.0.as_ref(), curr, name)?;
+ }
+ Ok(curr)
+ }
+}
+
+/// APIs when data can be modified.
+impl<T: AsMut<[u8]> + AsRef<[u8]>> Fdt<T> {
+ pub fn new_from_init(mut fdt: T, init: &[u8]) -> Result<Self> {
+ fdt_check_header(init)?;
+ // SAFETY: API from libfdt_c.
+ map_result(unsafe {
+ libfdt_c_def::fdt_move(
+ init.as_ptr() as *const _,
+ fdt.as_mut().as_ptr() as *mut _,
+ fdt.as_mut().len().try_into().map_err(|_| FdtError::IntegerOverflow)?,
+ )
+ })?;
+ let new_size: u32 = fdt.as_mut().len().try_into().map_err(|_| FdtError::IntegerOverflow)?;
+ let mut ret = Fdt::new(fdt)?;
+ ret.header_mut()?.set_totalsize(new_size);
+ Ok(ret)
+ }
+
+ /// Parse and get the FDT header.
+ fn header_mut(&mut self) -> Result<&mut FdtHeader> {
+ FdtHeader::from_bytes_mut(self.0.as_mut())
+ }
+
+ /// Reduce the total size field in the header to minimum that will fit existing content.
+ /// No more data can be added to the FDT. This should be called after all modification is
+ /// done and before passing to the kernel. This is to prevent kernel hang when FDT size is too
+ /// big.
+ pub fn shrink_to_fit(&mut self) -> Result<()> {
+ let actual = self.header_ref()?.actual_size();
+ self.header_mut()?.set_totalsize(actual.try_into().unwrap());
+ Ok(())
+ }
+
+ /// Set the value of a node's property. Create the node and property if it doesn't exist.
+ pub fn set_property(&mut self, path: &str, name: &CStr, val: &[u8]) -> Result<()> {
+ let node = self.find_or_add_node(path)?;
+ // SAFETY: API from libfdt_c.
+ map_result(unsafe {
+ fdt_setprop(
+ self.0.as_mut().as_mut_ptr() as *mut _,
+ node,
+ name.to_bytes_with_nul().as_ptr() as *const _,
+ val.as_ptr() as *const _,
+ val.len().try_into().map_err(|_| FdtError::IntegerOverflow)?,
+ )
+ })?;
+ Ok(())
+ }
+
+ /// Wrapper/equivalent of fdt_setprop_placeholder.
+ /// It creates/resizes a node's property to the given size and returns the buffer for caller
+ /// to modify content.
+ pub fn set_property_placeholder(
+ &mut self,
+ path: &str,
+ name: &CStr,
+ len: usize,
+ ) -> Result<&mut [u8]> {
+ let node = self.find_or_add_node(path)?;
+ let mut out_ptr: *mut u8 = core::ptr::null_mut();
+ // SAFETY: API from libfdt_c.
+ map_result(unsafe {
+ fdt_setprop_placeholder(
+ self.0.as_mut().as_mut_ptr() as *mut _,
+ node,
+ name.to_bytes_with_nul().as_ptr() as *const _,
+ len.try_into().map_err(|_| FdtError::IntegerOverflow)?,
+ &mut out_ptr as *mut *mut u8 as *mut _,
+ )
+ })?;
+ assert!(!out_ptr.is_null());
+ // SAFETY: Buffer returned by API from libfdt_c.
+ Ok(unsafe { from_raw_parts_mut(out_ptr, len) })
+ }
+
+ /// Find the offset of a node by a given node path. Add if node does not exist.
+ fn find_or_add_node(&mut self, path: &str) -> Result<core::ffi::c_int> {
+ let mut curr: core::ffi::c_int = 0;
+ for name in path.split('/') {
+ if name.len() == 0 {
+ continue;
+ }
+ curr = match fdt_subnode_offset(self.0.as_ref(), curr, name) {
+ Ok(v) => v,
+ _ => fdt_add_subnode(self.0.as_mut(), curr, name)?,
+ };
+ }
+ Ok(curr)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use std::ffi::CString;
+
+ fn to_cstr(s: &str) -> CString {
+ CString::new(s).unwrap()
+ }
+
+ #[test]
+ fn test_new_from_invalid_fdt() {
+ let mut init = include_bytes!("../test/test.dtb").to_vec();
+ let mut fdt_buf = vec![0u8; init.len()];
+ // Invalid total size
+ assert!(Fdt::new_from_init(&mut fdt_buf[..], &init[..init.len() - 1]).is_err());
+ // Invalid FDT
+ init[..4].fill(0);
+ assert!(Fdt::new_from_init(&mut fdt_buf[..], &init[..]).is_err());
+ }
+
+ #[test]
+ fn test_get_property() {
+ let init = include_bytes!("../test/test.dtb").to_vec();
+ let mut fdt_buf = vec![0u8; init.len()];
+ let fdt = Fdt::new_from_init(&mut fdt_buf[..], &init[..]).unwrap();
+
+ assert_eq!(
+ CStr::from_bytes_with_nul(fdt.get_property("/", &to_cstr("info")).unwrap())
+ .unwrap()
+ .to_str()
+ .unwrap(),
+ "test device tree"
+ );
+ assert_eq!(
+ CStr::from_bytes_with_nul(
+ fdt.get_property("/dev-2/dev-2.2/dev-2.2.1", &to_cstr("property-1")).unwrap()
+ )
+ .unwrap()
+ .to_str()
+ .unwrap(),
+ "dev-2.2.1-property-1"
+ );
+
+ // Non eixsts
+ assert!(fdt.get_property("/", &to_cstr("non-exist")).is_err());
+ }
+
+ #[test]
+ fn test_set_property() {
+ let init = include_bytes!("../test/test.dtb").to_vec();
+ let mut fdt_buf = vec![0u8; init.len() + 512];
+ let mut fdt = Fdt::new_from_init(&mut fdt_buf[..], &init[..]).unwrap();
+ let data = vec![0x11u8, 0x22u8, 0x33u8];
+ fdt.set_property("/new-node", &to_cstr("custom"), &data).unwrap();
+ assert_eq!(fdt.get_property("/new-node", &to_cstr("custom")).unwrap().to_vec(), data);
+ }
+
+ #[test]
+ fn test_set_property_placeholder() {
+ let init = include_bytes!("../test/test.dtb").to_vec();
+ let mut fdt_buf = vec![0u8; init.len() + 512];
+ let mut fdt = Fdt::new_from_init(&mut fdt_buf[..], &init[..]).unwrap();
+ let data = vec![0x11u8, 0x22u8, 0x33u8, 0x44u8, 0x55u8];
+ let payload =
+ fdt.set_property_placeholder("/new-node", &to_cstr("custom"), data.len()).unwrap();
+ payload.clone_from_slice(&data[..]);
+ assert_eq!(fdt.get_property("/new-node", &to_cstr("custom")).unwrap().to_vec(), data);
+ }
+
+ #[test]
+ fn test_header_from_raw() {
+ let init = include_bytes!("../test/test.dtb").to_vec();
+ // Pointer points to `init`
+ let (header, bytes) = unsafe { FdtHeader::from_raw(init.as_ptr()).unwrap() };
+ assert_eq!(header.totalsize(), init.len());
+ assert_eq!(bytes.to_vec(), init);
+ }
+
+ #[test]
+ fn test_header_from_raw_invalid() {
+ let mut init = include_bytes!("../test/test.dtb").to_vec();
+ init[..4].fill(0);
+ // Pointer points to `init`
+ assert!(unsafe { FdtHeader::from_raw(init.as_ptr()).is_err() });
+ }
+
+ #[test]
+ fn test_fdt_shrink_to_fit() {
+ let init = include_bytes!("../test/test.dtb").to_vec();
+ let mut fdt_buf = vec![0u8; init.len() + 512];
+ let fdt_buf_len = fdt_buf.len();
+ let mut fdt = Fdt::new_from_init(&mut fdt_buf[..], &init[..]).unwrap();
+ assert_eq!(fdt.size().unwrap(), fdt_buf_len);
+ fdt.shrink_to_fit().unwrap();
+ assert_eq!(fdt.size().unwrap(), init.len());
+ }
+}
diff --git a/gbl/libfdt/test/BUILD b/gbl/libfdt/test/BUILD
new file mode 100644
index 0000000..6325cad
--- /dev/null
+++ b/gbl/libfdt/test/BUILD
@@ -0,0 +1,15 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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.
+
+exports_files(glob(["**/*"]))
diff --git a/gbl/libfdt/test/gen_test_dtb.sh b/gbl/libfdt/test/gen_test_dtb.sh
new file mode 100755
index 0000000..a514265
--- /dev/null
+++ b/gbl/libfdt/test/gen_test_dtb.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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.
+
+set -e
+
+readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+
+dtc -I dts -O dtb -o ${SCRIPT_DIR}/test.dtb ${SCRIPT_DIR}/test.dts
diff --git a/gbl/libfdt/test/test.dtb b/gbl/libfdt/test/test.dtb
new file mode 100644
index 0000000..4aa1685
--- /dev/null
+++ b/gbl/libfdt/test/test.dtb
Binary files differ
diff --git a/gbl/libfdt/test/test.dts b/gbl/libfdt/test/test.dts
new file mode 100644
index 0000000..f4df645
--- /dev/null
+++ b/gbl/libfdt/test/test.dts
@@ -0,0 +1,21 @@
+/dts-v1/;
+
+/ {
+ info = "test device tree";
+
+ dev-1 {};
+
+ dev-2 {
+ dev-2.1 {};
+
+ dev-2.2 {
+ dev-2.2.1 {
+ property-1 = "dev-2.2.1-property-1";
+ };
+ };
+
+ dev-1.3 {};
+ };
+
+ dev-3 {};
+};
diff --git a/gbl/tests/BUILD b/gbl/tests/BUILD
index 0529101..6cad017 100644
--- a/gbl/tests/BUILD
+++ b/gbl/tests/BUILD
@@ -16,6 +16,7 @@
name = "tests",
tests = [
"@gbl//libefi:libefi_test",
+ "@gbl//libfdt:libfdt_test",
"@gbl//libstorage:gbl_libstorage_test",
"@gbl//third_party/libzbi:libzbi_test",
diff --git a/gbl/toolchain/BUILD b/gbl/toolchain/BUILD
index 2f9d5e3..1e4c741 100644
--- a/gbl/toolchain/BUILD
+++ b/gbl/toolchain/BUILD
@@ -173,7 +173,7 @@
name = "x86_64_uefi_clang_cc_toolchain",
cc_flags = cc_flags_common,
target_cpu = "x86_64",
- target_system_triple = "x86_64-windows-msvc",
+ target_system_triple = "x86_64-unknown-windows-msvc",
)
toolchain(
@@ -189,7 +189,7 @@
name = "x86_32_uefi_clang_cc_toolchain",
cc_flags = cc_flags_common,
target_cpu = "x86_32",
- target_system_triple = "x86-windows-msvc",
+ target_system_triple = "i686-unknown-windows-gnu",
)
toolchain(
diff --git a/gbl/toolchain/gbl_toolchain.bzl b/gbl/toolchain/gbl_toolchain.bzl
index 14583c0..41167da 100644
--- a/gbl/toolchain/gbl_toolchain.bzl
+++ b/gbl/toolchain/gbl_toolchain.bzl
@@ -25,7 +25,7 @@
"tool_path",
)
load("@gbl_llvm_prebuilts//:info.bzl", "LLVM_PREBUILTS_C_INCLUDE", "gbl_llvm_tool_path")
-load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES", "ALL_CC_COMPILE_ACTION_NAMES")
+load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES", "ALL_CPP_COMPILE_ACTION_NAMES")
def _flag_set(flags):
"""Convert a list of compile/link flags to a flag_set type."""
@@ -40,6 +40,15 @@
def _gbl_clang_cc_toolchain_config_impl(ctx):
"""Implementation for rule _gbl_clang_cc_toolchain_config()"""
builtin_includes = ctx.attr.builtin_includes or [LLVM_PREBUILTS_C_INCLUDE]
+ common_compile_flagset = [
+ _flag_set([
+ "--target={}".format(ctx.attr.target_system_triple),
+ "-nostdinc",
+ "-no-canonical-prefixes",
+ "-Werror",
+ "-Wall",
+ ] + ["-I{}".format(inc) for inc in builtin_includes] + ctx.attr.cc_flags),
+ ]
return cc_common.create_cc_toolchain_config_info(
ctx = ctx,
cxx_builtin_include_directories = builtin_includes,
@@ -56,17 +65,17 @@
action_name = action_name,
enabled = True,
tools = [tool(path = gbl_llvm_tool_path("clang++"))],
- flag_sets = [
- _flag_set([
- "--target={}".format(ctx.attr.target_system_triple),
- "-nostdinc",
- "-no-canonical-prefixes",
- "-Werror",
- "-Wall",
- ] + ["-I{}".format(inc) for inc in builtin_includes] + ctx.attr.cc_flags),
- ],
+ flag_sets = common_compile_flagset,
)
- for action_name in ALL_CC_COMPILE_ACTION_NAMES
+ for action_name in ALL_CPP_COMPILE_ACTION_NAMES +
+ [ACTION_NAMES.assemble, ACTION_NAMES.preprocess_assemble]
+ ] + [
+ action_config(
+ action_name = ACTION_NAMES.c_compile,
+ enabled = True,
+ tools = [tool(path = gbl_llvm_tool_path("clang"))],
+ flag_sets = common_compile_flagset,
+ ),
] + [
action_config(
action_name = ACTION_NAMES.cpp_link_executable,
@@ -248,3 +257,44 @@
"deps": attr.label_list(allow_files = True, mandatory = True),
},
)
+
+# This rule creates symlink for a static library in both Linux/GNU and MSVC naming styles so that
+# rust linker is able to find it when building for them.
+#
+# When flag "-Clink-arg=-l<libname>" is passed to rustc, for Linux/GNU target platforms, the linker
+# looks for library named "lib<libname>.a", for MSVC target plaforms (i.e. UEFI), the linker looks
+# for library named "<libname>.lib". When bazel builds a cc_library target, it always outputs the
+# Linux/GNU naming style and therefore fails linking when building for UEFI targets.
+def _link_static_cc_library_impl(ctx):
+ # Put an underscore so that we don't need to deal with potential "lib" prefix from user
+ # provided name.
+ libname = "_{}".format(ctx.label.name)
+
+ # Create symlink for both naming styles.
+ out_msvc_style = ctx.actions.declare_file("{}.lib".format(libname))
+ ctx.actions.symlink(output = out_msvc_style, target_file = ctx.files.cc_library[0])
+ out_linux_style = ctx.actions.declare_file("lib{}.a".format(libname))
+ ctx.actions.symlink(output = out_linux_style, target_file = ctx.files.cc_library[0])
+
+ # Construct and return a `CcInfo` for this rule that includes the library to link, so that
+ # other rust_library/cc_library can depend directly on this target.
+ library_to_link = cc_common.create_library_to_link(
+ actions = ctx.actions,
+ # Either is fine, since both yield the same linker option by Bazel.
+ static_library = out_linux_style,
+ )
+ linking_input = cc_common.create_linker_input(
+ owner = ctx.label,
+ libraries = depset([library_to_link]),
+ # Make sure both symlinks are generated.
+ additional_inputs = depset([out_msvc_style, out_linux_style]),
+ )
+ linking_context = cc_common.create_linking_context(linker_inputs = depset([linking_input]))
+ return [CcInfo(linking_context = linking_context)]
+
+link_static_cc_library = rule(
+ implementation = _link_static_cc_library_impl,
+ attrs = {
+ "cc_library": attr.label(), # The cc_library() target for the static library.
+ },
+)