0

How do I set the UUID of an ExFat partition?

The linux tools I've tried (mlabel, gparted) only allow resetting the UUID to a different random one, not to a UUID I choose.

1 Answer 1

0

The tools I could find couldn't do this, so I wrote my own. See https://github.com/JanKanis/exfat_setuuid.

Later on I found that on Linux, tune.exfat and exfatlabel (part of exfatprogs) can also do it.

It can also display some low level ExFat configuration which can come in handy if you want to e.g. recreate an ExFat partition with a smaller size but with the same cluster sizes and FAT table offset, as those are sometimes optimized for the specific device.

In case the link ever breaks, here's the full file:

#!/usr/bin/env python3

import sys, os, re, argparse, pathlib, subprocess, weakref
from collections import namedtuple

try:
    from humanfriendly import parse_size, format_size
except ImportError:
    def parse_size(num, binary=False):
        return int(num)
    format_size = None



DEFAULT_BLOCK_SIZE = 512

# Volume Boot Record / Volume Boot Region
BACKUP_VBR_OFFSET = 12

CHECKSUM_OFFSET = 11    # no. of sectors relative to start of VBR
CHECKSUMMED_DATA_LENGTH = CHECKSUM_OFFSET

FILE_SYSTEM_NAME = b'EXFAT   '  # mind the spaces
BOOT_SIGNATURE = 0xAA55



def get_uuid_bytes(uuid_str):
    if not re.fullmatch('[0-9a-fA-F]{4}-[0-9a-fA-F]{4}', uuid_str):
        raise ValueError("invalid UUID, expected format: 12AB-E9FF")
        
    bytes_str = [uuid_str[7:9], uuid_str[5:7], uuid_str[2:4], uuid_str[0:2]]
    return bytes(int(x, 16) for x in bytes_str)

def uuid_str(uuid_num):
    b1, b2, b3, b4 = (format(b, '02X') for b in reversed(uuid_num.to_bytes(4, byteorder='little')))
    return f"{b1+b2}-{b3+b4}"


def try_get_block_size(device):
    try:
        p = subprocess.run(['blockdev', '--getbsz', device], capture_output=True, encoding='utf8')
        if p.returncode == 0:
            return int(p.stdout)
        else:
            print("Unable to determine device block size, defaulting to 512: "+p.stderr)
            return None
    except (FileNotFoundError, ValueError):
        return None


class ExFatFS:
    def __init__(self, file, sector_size=None):
        self.inconsistent = False
        self.file = file
        self.vbr = VBR(self.file, 0, self, sector_size=sector_size)
        if sector_size is None:
            sector_size = self.vbr.sector_size
        self.backup_vbr = VBR(self.file, BACKUP_VBR_OFFSET*sector_size, self, sector_size=sector_size, is_backup=True)

    def set_uuid(self, uuid):
        self.vbr.set_uuid(uuid)
        self.file.flush()
        os.fsync(self.file.fileno())  # Update main and backup VBR one by one for recoverability
        self.backup_vbr.set_uuid(uuid)
        self.file.flush()
        os.fsync(self.file.fileno())
        
    def check(self):
        self.vbr.check()
        self.backup_vbr.check()
        for field, desc in VBR.fields.items():
            if field in {'volume_flags', 'percent_in_use'}:
                continue
            if not getattr(self.vbr, field) == getattr(self.backup_vbr, field):
                self.inconsistentFS(f"Invalid EXFAT filesystem: {field} in VBR does not equal {field} in backup VBR. Found {VBR.format_value(getattr(self.vbr, field), desc[1])} and {VBR.format_value(getattr(self.backup_vbr, field), desc[1])}")


    def inconsistentFS(self, message):
        self.inconsistent = True
        print(message, file=sys.stderr)



D = Desc = namedtuple('Desc', 'offset unit size', defaults=[4])
class VBR:

    fields = dict(
        # name (as in spec)              offset   unit      size (bytes, default 4)
        partition_offset =                D(64,  'sectors', 8),
        volume_length =                   D(72,  'sectors', 8),
        fat_offset =                      D(80,  'sectors'),
        fat_length =                      D(84,  'sectors'),
        cluster_heap_offset =             D(88,  'sectors'),
        cluster_count =                   D(92,  'clusters'),
        first_cluster_of_root_directory = D(96,  'clusters'),
        volume_serial_number =            D(100, 'uuid'),
        file_system_revision =            D(104, 'version', 2),
        volume_flags =                    D(106, 'flags', 2),
        bytes_per_sector_shift =          D(108, 'log2', 1),
        sectors_per_cluster_shift =       D(109, 'log2', 1),
        number_of_fats =                  D(110, 'number', 1),
        drive_select =                    D(111, 'number', 1),
        percent_in_use =                  D(112, 'number', 1),
        boot_signature =                  D(510, 'hex', 2),
    )
    
    calculated_fields = dict(
        **fields,
        checksum = D(None, 'hex'),
        bytes_per_sector = D(None, 'bytes'),
        bytes_per_cluster = D(None, 'bytes'),
        **{k+'Bytes': D(None, 'bytes') for k, v in fields.items() if v[1] == 'sectors'},
        cluster_heap_length_bytes = D(None, 'bytes'),
    )


    def __init__(self, file, offset, exfatfs, sector_size=None, is_backup=False):
        self.file = file
        self.offset = offset
        self.exfatfs = weakref.ref(exfatfs)
        self.sector_size = sector_size
        self.is_backup = is_backup

        self.read_data()


    def read_data(self):
        self.file.seek(self.offset)
        self.vbr = self.file.read(512)
        
        self.file_system_name = self.vbr[3:3+8]
        self._readfields()


    def _read(self, offset, length=4):
        return int.from_bytes(self.vbr[offset:offset+length], byteorder='little')
        

    def _readfields(self):
        sectorfields = []
        for name, desc in self.fields.items():
            offset, unit, size = desc

            value = self._read(offset, size)
            if unit == 'sectors':
                sectorfields.append(name)

            setattr(self, name, value)

        self.bytes_per_sector = 2**self.bytes_per_sector_shift
        self.bytes_per_cluster = self.bytes_per_sector * 2**self.sectors_per_cluster_shift

        if self.sector_size is None:
            self.sector_size = self.bytes_per_sector
            print(f"Using filesystem reported sector size of {self.sector_size} bytes", file=sys.stderr)

        self.checksum = self.get_checksum(report_bad=False)

        for fieldname in sectorfields:
            f = fieldname+'_bytes'
            setattr(self, f, getattr(self, fieldname) * self.bytes_per_sector)

        self.cluster_heap_length_bytes = self.cluster_count * self.bytes_per_cluster


    def get_checksum(self, report_bad=True):
        self.file.seek(self.offset+self.sector_size*CHECKSUM_OFFSET)
        checksum_block = self.file.read(self.sector_size)
        checksum_bytes = checksum_block[0:4]
        if report_bad and not checksum_block == checksum_bytes * (self.sector_size//4):
            self.inconsistentFS(f"Invalid EXFAT filesystem: Checksum block of {'backup ' if self.is_backup else ''}volume boot record is corrupt. Expecting a repetition of the checksum value.")
        return int.from_bytes(checksum_bytes, byteorder='little')


    def calc_checksum(self):
        self.file.seek(self.offset)
        data = self.file.read(CHECKSUMMED_DATA_LENGTH*self.sector_size)

        checksum = 0
        for i, byte in enumerate(data):
            if i in (106, 107, 112):
                continue
            checksum = (0x80000000 if (checksum & 1) else 0) + (checksum >> 1) + byte
            checksum &= 0xffffffff
        return checksum


    def write_checksum(self):
        checksum_sector = self.calc_checksum().to_bytes(4, byteorder='little') * (self.sector_size//4)
        self.file.seek(self.offset + self.sector_size*CHECKSUM_OFFSET)
        self.file.write(checksum_sector)


    def check(self):
        self.file.seek(self.offset+11)
        must_be_zero = self.file.read(53)

        if self.file_system_name != FILE_SYSTEM_NAME:
            self.inconsistentFS(f"Invalid EXFAT filesystem: name in {self._backup_str()}volume boot record. Found {self.file_system_name}, expected {FILE_SYSTEM_NAME}")

        if must_be_zero != b'\x00'*53:
            self.inconsistentFS(f"Invalid EXFAT filesystem: MustBeZero field in {self._backup_str()}volume boot record is not all zeros.")

        if self.boot_signature != BOOT_SIGNATURE:
            self.inconsistentFS(f"Invalid EXFAT filesystem: boot signature in {self._backup_str()}volume boot record is not 0x{BOOT_SIGNATURE:X}. Found 0x{self.boot_signature:X}.")
        
        if self.bytes_per_sector != self.sector_size:
            self.inconsistentFS(f"Invalid EXFAT filesystem: Using a blocksize of {self.sector_size}, but {self._backup_str()}volume boot record says blocksize is {self.bytes_per_sector}")

        checksum = self.get_checksum()
        expected_checksum = self.calc_checksum()
        if checksum != expected_checksum:
            self.inconsistentFS(f"Invalid EXFAT filesystem: Invalid checksum for {self._backup_str()}volume boot record: expected {expected_checksum:x}, found {checksum:x}")


    def base_write_uuid(self, uuid):
        self.file.seek(self.offset+self.fields['volume_serial_number'].offset)
        self.file.write(uuid)


    def set_uuid(self, uuid):
        self.base_write_uuid(uuid)
        self.write_checksum()
        self.read_data()


    def inconsistentFS(self, message):
        self.exfatfs().inconsistentFS(message)

    def _backup_str(self):
        return 'backup ' if self.is_backup else ''


    def __str__(self):
        s = "FSInfo:\n"
        for name, desc in self.calculated_fields.items():
            val = getattr(self, name)
            s += f"  {name}: {self.format_value(val, desc.unit)}\n"
        return s


    @staticmethod
    def format_value(val, unit):
        if unit == 'uuid':
            return uuid_str(val)
        if unit == 'hex':
            return f"0x{val:X}"
        if unit == 'version':
            return f"{val//256}.{val%256}"
        if unit == 'flags':
            return ','.join(name for name, flag in (('ActiveFat', 1), ('VolumeDirty', 2), ('MediaFailure', 4), ('ClearToZero', 8), (f'Reserved=0x{val&0xfff0:X}', 0xfff0)) if flag&val) or '(none)'
        if unit == 'bytes' and format_size:
            return f"{val} ({format_size(val, binary=True)})"
        return str(val)     


class InconsistentExFatException(Exception):
    pass



def main():
    argp = argparse.ArgumentParser(description="This program shows low level configuration of an ExFat filesystem and checks the volume boot record (superblock) for consistecy. It also allows setting the UUID/serial number. Without options, will check consistency and show configuration.")
    argp.add_argument('device', type=pathlib.Path, help="The device file to use.")
    argp.add_argument('--write-uuid', dest='uuid', type=get_uuid_bytes, help="Write this UUID (serial number) to the filesystem superblock. Before writing, the program will verify the consistency of the filesystem superblock. WARNING: There should NEVER be more than one active filesystem with the same UUID on your system. This should only be used if you are replacing an old ExFat filesystem!")
    argp.add_argument('--read-device-sector-size', action='store_true', help='Use device block size (does not work on image files; this requires the `blockdev` program)')
    argp.add_argument('--sector-size', '-b', default=None, type=lambda x: parse_size(x, binary=True), help='The sector size of the file system. K-suffix is supported. If omitted, will read sector size from filesystem superblock.')
    argp.add_argument('--ignore-invalid', action='store_true', help='Report configuration even if filesystem is corrupt')
    args = argp.parse_args()

    mode = 'rb' if args.uuid is None else 'rb+'
    with open(args.device, mode) as file:
        if args.sector_size is None and args.read_device_sector_size:
            args.sector_size = try_get_block_size(args.device)

        fs = ExFatFS(file, sector_size=args.sector_size)
        fs.check()
        if fs.inconsistent:
            print(f"Error: Not an ExFat filesystem or filesystem is corrupted.", file=sys.stderr)
            if not args.ignore_invalid:
                sys.exit(1)

        if not args.uuid:
            print(fs.vbr)

        if not fs.inconsistent and args.uuid:
            fs.set_uuid(args.uuid)
            fs.check()
            print(f"Updated UUID to {uuid_str(int.from_bytes(args.uuid, byteorder='little'))}")


if __name__ == '__main__':
    main()

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .