blob: 840553b2db394a21ffd9a33c014bc04f8835b0a9 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Library for generating ChromeOS ConfigFS private data file."""
from __future__ import print_function
import enum
import json
import os
import struct
import subprocess
import tempfile
STRUCT_VERSION = 0
# version, identity_type, entry_count, 4 bytes reserved
HEADER_FORMAT = '<LLL4x'
# flags, model match, sku match, whitelabel match
ENTRY_FORMAT = '<LLLL'
class IdentityType(enum.Enum):
"""The type of identity provided by the identity data file."""
X86 = 0
ARM = 1
class EntryFlags(enum.Enum):
"""The flags used at the beginning of each entry."""
HAS_SKU_ID = 1 << 0
HAS_WHITELABEL = 1 << 1
# This device uses a customization ID from VPD to match instead of a
# whitelabel tag. This is deprecated for new devices since 2017, so
# it should only be set for old pre-unibuild migrations.
USES_CUSTOMIZATION_ID = 1 << 2
# For ARM only: use a portion of the FRID to match the device
# instead of a device-tree compatible string.
USES_FIRMWARE_NAME = 1 << 3
# For x86 only: this device has an SMBIOS name to match.
HAS_SMBIOS_NAME = 1 << 4
def Serialize(obj):
"""Convert a string, integer, bytes, or bool to its file representation.
Args:
obj: The string, integer, bytes, or bool to serialize.
Returns:
The bytes representation of the object suitable for dumping into a file.
"""
if isinstance(obj, bytes):
return obj
if isinstance(obj, bool):
return b'true' if obj else b'false'
return str(obj).encode('utf-8')
def WriteConfigFSFiles(config, base_path):
"""Recursive function to write ConfigFS data out to files and directories.
Args:
config: The configuration item (dict, list, str, int, or bool).
base_path: The path to write out to.
"""
if isinstance(config, dict):
iterator = config.items()
elif isinstance(config, list):
iterator = enumerate(config)
else:
iterator = None
if iterator is not None:
os.makedirs(base_path, mode=0o755)
for name, entry in iterator:
path = os.path.join(base_path, str(name))
WriteConfigFSFiles(entry, path)
else:
with open(os.open(base_path, os.O_CREAT | os.O_WRONLY, 0o644), 'wb') as f:
f.write(Serialize(config))
def WriteIdentityJson(config, output_file):
"""Write out the data file needed to provide system identification.
This data file is used at runtime by libcros_config. Currently, JSON is used
as the output format to remain compatible with the existing code in
libcros_config. However, this function may be updated if libcros_config
adopts a new format, so do not rely on the output being JSON.
Args:
config: The configuration dictionary (containing "chromeos").
output_file: A file-like object to write to.
"""
minified_configs = []
minified_config = {'chromeos': {'configs': minified_configs}}
for device_config in config['chromeos']['configs']:
minified_configs.append({'identity': device_config['identity']})
json.dump(minified_config, output_file)
def WriteIdentityStruct(config, output_file):
"""Write out the data file needed to provide system identification.
This data file is used at runtime by cros_configfs to probe the
identity of the device. The struct must align with the C code in
cros_configfs.
Args:
config: The configuration dictionary (containing "chromeos").
output_file: A file-like object to write to, opened in binary mode.
"""
device_configs = config['chromeos']['configs']
string_table = []
# Add a string to the table if it does to exist. Return the number
# of bytes offset the string will live from the base of the string
# table.
def _StringTableIndex(string):
if string is None:
return 0
string = string.lower()
string = string.encode('utf-8') + b'\000'
if string not in string_table:
string_table.append(string)
index = 0
for entry in string_table:
if entry == string:
return index
index += len(entry)
# Detecting x86 vs. ARM is rather annoying given the JSON-like
# schema. This implementation checks if any config has an identity
# dict containing a device-tree-compatible-match or firmware-name,
# and correspondingly sets the identity type to ARM, otherwise X86.
if any('device-tree-compatible-match' in c.get('identity', {})
or 'firmware-name' in c.get('identity', {})
for c in device_configs):
identity_type = IdentityType.ARM
else:
identity_type = IdentityType.X86
# Write the header of the struct, containing version and identity
# type (x86 vs. ARM).
output_file.write(
struct.pack(HEADER_FORMAT, STRUCT_VERSION, identity_type.value,
len(device_configs)))
# Write each of the entry structs.
for device_config in device_configs:
identity_info = device_config.get('identity', {})
flags = 0
sku_id = 0
if 'sku-id' in identity_info:
flags |= EntryFlags.HAS_SKU_ID.value
sku_id = identity_info['sku-id']
model_match = None
whitelabel_match = None
if identity_type is IdentityType.X86:
if 'smbios-name-match' in identity_info:
flags |= EntryFlags.HAS_SMBIOS_NAME.value
model_match = identity_info['smbios-name-match']
elif identity_type is IdentityType.ARM:
if 'firmware-name' in identity_info:
flags |= EntryFlags.USES_FIRMWARE_NAME.value
model_match = identity_info['firmware-name']
else:
model_match = identity_info['device-tree-compatible-match']
if 'customization-id' in identity_info:
flags |= EntryFlags.USES_CUSTOMIZATION_ID.value
whitelabel_match = identity_info['customization-id']
elif 'whitelabel-tag' in identity_info:
flags |= EntryFlags.HAS_WHITELABEL.value
whitelabel_match = identity_info['whitelabel-tag']
output_file.write(
struct.pack(ENTRY_FORMAT,
flags,
_StringTableIndex(model_match),
sku_id,
_StringTableIndex(whitelabel_match)))
for entry in string_table:
output_file.write(entry)
def GenerateConfigFSData(config, output_fs):
"""Generate the ConfigFS private data.
Args:
config: The configuration dictionary.
output_fs: The file name to write the SquashFS image at.
"""
with tempfile.TemporaryDirectory(prefix='configfs.') as configdir:
os.chmod(configdir, 0o755)
WriteConfigFSFiles(config, os.path.join(configdir, 'v1'))
# TODO(jrosenth): remove the json file once we've converted to the
# struct format. Both files are included now as a transitional
# scheme.
with open(os.path.join(configdir, 'v1/identity.json'), 'w') as f:
WriteIdentityJson(config, f)
with open(os.path.join(configdir, 'identity.bin'), 'wb') as f:
WriteIdentityStruct(config, f)
subprocess.run(['mksquashfs', configdir, output_fs, '-no-xattrs',
'-noappend', '-all-root'], check=True,
stdout=subprocess.PIPE)