github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/gadget/layout.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package gadget
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"sort"
    27  
    28  	"github.com/snapcore/snapd/gadget/quantity"
    29  )
    30  
    31  // LayoutConstraints defines the constraints for arranging structures within a
    32  // volume
    33  type LayoutConstraints struct {
    34  	// NonMBRStartOffset is the default start offset of non-MBR structure in
    35  	// the volume.
    36  	NonMBRStartOffset quantity.Offset
    37  	// SectorSize is the size of the sector to be used for calculations
    38  	SectorSize quantity.Size
    39  }
    40  
    41  // LaidOutVolume defines the size of a volume and arrangement of all the
    42  // structures within it
    43  type LaidOutVolume struct {
    44  	*Volume
    45  	// Size is the total size of the volume
    46  	Size quantity.Size
    47  	// SectorSize sector size of the volume
    48  	SectorSize quantity.Size
    49  	// LaidOutStructure is a list of structures within the volume, sorted
    50  	// by their start offsets
    51  	LaidOutStructure []LaidOutStructure
    52  	// RootDir is the root directory for volume data
    53  	RootDir string
    54  }
    55  
    56  // PartiallyLaidOutVolume defines the layout of volume structures, but lacks the
    57  // details about the layout of raw image content within the bare structures.
    58  type PartiallyLaidOutVolume struct {
    59  	*Volume
    60  	// SectorSize sector size of the volume
    61  	SectorSize quantity.Size
    62  	// LaidOutStructure is a list of structures within the volume, sorted
    63  	// by their start offsets
    64  	LaidOutStructure []LaidOutStructure
    65  }
    66  
    67  // LaidOutStructure describes a VolumeStructure that has been placed within the
    68  // volume
    69  type LaidOutStructure struct {
    70  	*VolumeStructure
    71  	// StartOffset defines the start offset of the structure within the
    72  	// enclosing volume
    73  	StartOffset quantity.Offset
    74  	// AbsoluteOffsetWrite is the resolved absolute position of offset-write
    75  	// for this structure element within the enclosing volume
    76  	AbsoluteOffsetWrite *quantity.Offset
    77  	// Index of the structure definition in gadget YAML
    78  	Index int
    79  	// LaidOutContent is a list of raw content inside the structure
    80  	LaidOutContent []LaidOutContent
    81  }
    82  
    83  func (p LaidOutStructure) String() string {
    84  	return fmtIndexAndName(p.Index, p.Name)
    85  }
    86  
    87  type byStartOffset []LaidOutStructure
    88  
    89  func (b byStartOffset) Len() int           { return len(b) }
    90  func (b byStartOffset) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
    91  func (b byStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
    92  
    93  // LaidOutContent describes raw content that has been placed within the
    94  // encompassing structure and volume
    95  type LaidOutContent struct {
    96  	*VolumeContent
    97  
    98  	// StartOffset defines the start offset of this content image
    99  	StartOffset quantity.Offset
   100  	// AbsoluteOffsetWrite is the resolved absolute position of offset-write
   101  	// for this content element within the enclosing volume
   102  	AbsoluteOffsetWrite *quantity.Offset
   103  	// Size is the maximum size occupied by this image
   104  	Size quantity.Size
   105  	// Index of the content in structure declaration inside gadget YAML
   106  	Index int
   107  }
   108  
   109  func (p LaidOutContent) String() string {
   110  	if p.Image != "" {
   111  		return fmt.Sprintf("#%v (%q@%#x{%v})", p.Index, p.Image, p.StartOffset, p.Size)
   112  	}
   113  	return fmt.Sprintf("#%v (source:%q)", p.Index, p.UnresolvedSource)
   114  }
   115  
   116  func layoutVolumeStructures(volume *Volume, constraints LayoutConstraints) (structures []LaidOutStructure, byName map[string]*LaidOutStructure, err error) {
   117  	previousEnd := quantity.Offset(0)
   118  	structures = make([]LaidOutStructure, len(volume.Structure))
   119  	byName = make(map[string]*LaidOutStructure, len(volume.Structure))
   120  
   121  	if constraints.SectorSize == 0 {
   122  		return nil, nil, fmt.Errorf("cannot lay out volume, invalid constraints: sector size cannot be 0")
   123  	}
   124  
   125  	for idx, s := range volume.Structure {
   126  		var start quantity.Offset
   127  		if s.Offset == nil {
   128  			if s.Role != schemaMBR && previousEnd < constraints.NonMBRStartOffset {
   129  				start = constraints.NonMBRStartOffset
   130  			} else {
   131  				start = previousEnd
   132  			}
   133  		} else {
   134  			start = *s.Offset
   135  		}
   136  
   137  		end := start + quantity.Offset(s.Size)
   138  		ps := LaidOutStructure{
   139  			VolumeStructure: &volume.Structure[idx],
   140  			StartOffset:     start,
   141  			Index:           idx,
   142  		}
   143  
   144  		if ps.Role != schemaMBR {
   145  			if s.Size%constraints.SectorSize != 0 {
   146  				return nil, nil, fmt.Errorf("cannot lay out volume, structure %v size is not a multiple of sector size %v",
   147  					ps, constraints.SectorSize)
   148  			}
   149  		}
   150  
   151  		if ps.Name != "" {
   152  			byName[ps.Name] = &ps
   153  		}
   154  
   155  		structures[idx] = ps
   156  
   157  		previousEnd = end
   158  	}
   159  
   160  	// sort by starting offset
   161  	sort.Sort(byStartOffset(structures))
   162  
   163  	previousEnd = quantity.Offset(0)
   164  	for idx, ps := range structures {
   165  		if ps.StartOffset < previousEnd {
   166  			return nil, nil, fmt.Errorf("cannot lay out volume, structure %v overlaps with preceding structure %v", ps, structures[idx-1])
   167  		}
   168  		previousEnd = ps.StartOffset + quantity.Offset(ps.Size)
   169  
   170  		offsetWrite, err := resolveOffsetWrite(ps.OffsetWrite, byName)
   171  		if err != nil {
   172  			return nil, nil, fmt.Errorf("cannot resolve offset-write of structure %v: %v", ps, err)
   173  		}
   174  		structures[idx].AbsoluteOffsetWrite = offsetWrite
   175  	}
   176  
   177  	return structures, byName, nil
   178  }
   179  
   180  // LayoutVolumePartially attempts to lay out only the structures in the volume using provided constraints
   181  func LayoutVolumePartially(volume *Volume, constraints LayoutConstraints) (*PartiallyLaidOutVolume, error) {
   182  	structures, _, err := layoutVolumeStructures(volume, constraints)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	vol := &PartiallyLaidOutVolume{
   187  		Volume:           volume,
   188  		SectorSize:       constraints.SectorSize,
   189  		LaidOutStructure: structures,
   190  	}
   191  	return vol, nil
   192  }
   193  
   194  // LayoutVolume attempts to completely lay out the volume, that is the
   195  // structures and their content, using provided constraints
   196  func LayoutVolume(gadgetRootDir string, volume *Volume, constraints LayoutConstraints) (*LaidOutVolume, error) {
   197  
   198  	structures, byName, err := layoutVolumeStructures(volume, constraints)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	farthestEnd := quantity.Offset(0)
   204  	fartherstOffsetWrite := quantity.Offset(0)
   205  
   206  	for idx, ps := range structures {
   207  		if ps.AbsoluteOffsetWrite != nil && *ps.AbsoluteOffsetWrite > fartherstOffsetWrite {
   208  			fartherstOffsetWrite = *ps.AbsoluteOffsetWrite
   209  		}
   210  		if end := ps.StartOffset + quantity.Offset(ps.Size); end > farthestEnd {
   211  			farthestEnd = end
   212  		}
   213  
   214  		content, err := layOutStructureContent(gadgetRootDir, &structures[idx], byName)
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  
   219  		for _, c := range content {
   220  			if c.AbsoluteOffsetWrite != nil && *c.AbsoluteOffsetWrite > fartherstOffsetWrite {
   221  				fartherstOffsetWrite = *c.AbsoluteOffsetWrite
   222  			}
   223  		}
   224  
   225  		structures[idx].LaidOutContent = content
   226  	}
   227  
   228  	volumeSize := quantity.Size(farthestEnd)
   229  	if fartherstOffsetWrite+quantity.Offset(SizeLBA48Pointer) > farthestEnd {
   230  		volumeSize = quantity.Size(fartherstOffsetWrite) + SizeLBA48Pointer
   231  	}
   232  
   233  	vol := &LaidOutVolume{
   234  		Volume:           volume,
   235  		Size:             volumeSize,
   236  		SectorSize:       constraints.SectorSize,
   237  		LaidOutStructure: structures,
   238  		RootDir:          gadgetRootDir,
   239  	}
   240  	return vol, nil
   241  }
   242  
   243  type byContentStartOffset []LaidOutContent
   244  
   245  func (b byContentStartOffset) Len() int           { return len(b) }
   246  func (b byContentStartOffset) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   247  func (b byContentStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
   248  
   249  func getImageSize(path string) (quantity.Size, error) {
   250  	stat, err := os.Stat(path)
   251  	if err != nil {
   252  		return 0, err
   253  	}
   254  	return quantity.Size(stat.Size()), nil
   255  }
   256  
   257  func layOutStructureContent(gadgetRootDir string, ps *LaidOutStructure, known map[string]*LaidOutStructure) ([]LaidOutContent, error) {
   258  	if ps.HasFilesystem() {
   259  		// structures with a filesystem do not need any extra layout
   260  		return nil, nil
   261  	}
   262  	if len(ps.Content) == 0 {
   263  		return nil, nil
   264  	}
   265  
   266  	content := make([]LaidOutContent, len(ps.Content))
   267  	previousEnd := quantity.Offset(0)
   268  
   269  	for idx, c := range ps.Content {
   270  		imageSize, err := getImageSize(filepath.Join(gadgetRootDir, c.Image))
   271  		if err != nil {
   272  			return nil, fmt.Errorf("cannot lay out structure %v: content %q: %v", ps, c.Image, err)
   273  		}
   274  
   275  		var start quantity.Offset
   276  		if c.Offset != nil {
   277  			start = *c.Offset
   278  		} else {
   279  			start = previousEnd
   280  		}
   281  
   282  		actualSize := imageSize
   283  
   284  		if c.Size != 0 {
   285  			if c.Size < imageSize {
   286  				return nil, fmt.Errorf("cannot lay out structure %v: content %q size %v is larger than declared %v", ps, c.Image, actualSize, c.Size)
   287  			}
   288  			actualSize = c.Size
   289  		}
   290  
   291  		offsetWrite, err := resolveOffsetWrite(c.OffsetWrite, known)
   292  		if err != nil {
   293  			return nil, fmt.Errorf("cannot resolve offset-write of structure %v content %q: %v", ps, c.Image, err)
   294  		}
   295  
   296  		content[idx] = LaidOutContent{
   297  			VolumeContent: &ps.Content[idx],
   298  			Size:          actualSize,
   299  			StartOffset:   ps.StartOffset + start,
   300  			Index:         idx,
   301  			// break for gofmt < 1.11
   302  			AbsoluteOffsetWrite: offsetWrite,
   303  		}
   304  		previousEnd = start + quantity.Offset(actualSize)
   305  		if quantity.Size(previousEnd) > ps.Size {
   306  			return nil, fmt.Errorf("cannot lay out structure %v: content %q does not fit in the structure", ps, c.Image)
   307  		}
   308  	}
   309  
   310  	sort.Sort(byContentStartOffset(content))
   311  
   312  	previousEnd = ps.StartOffset
   313  	for idx, pc := range content {
   314  		if pc.StartOffset < previousEnd {
   315  			return nil, fmt.Errorf("cannot lay out structure %v: content %q overlaps with preceding image %q", ps, pc.Image, content[idx-1].Image)
   316  		}
   317  		previousEnd = pc.StartOffset + quantity.Offset(pc.Size)
   318  	}
   319  
   320  	return content, nil
   321  }
   322  
   323  func resolveOffsetWrite(offsetWrite *RelativeOffset, knownStructs map[string]*LaidOutStructure) (*quantity.Offset, error) {
   324  	if offsetWrite == nil {
   325  		return nil, nil
   326  	}
   327  
   328  	var relativeToOffset quantity.Offset
   329  	if offsetWrite.RelativeTo != "" {
   330  		otherStruct, ok := knownStructs[offsetWrite.RelativeTo]
   331  		if !ok {
   332  			return nil, fmt.Errorf("refers to an unknown structure %q", offsetWrite.RelativeTo)
   333  		}
   334  		relativeToOffset = otherStruct.StartOffset
   335  	}
   336  
   337  	resolvedOffsetWrite := relativeToOffset + offsetWrite.Offset
   338  	return &resolvedOffsetWrite, nil
   339  }
   340  
   341  // ShiftStructureTo translates the starting offset of a laid out structure and
   342  // its content to the provided offset.
   343  func ShiftStructureTo(ps LaidOutStructure, offset quantity.Offset) LaidOutStructure {
   344  	change := int64(offset - ps.StartOffset)
   345  
   346  	newPs := ps
   347  	newPs.StartOffset = quantity.Offset(int64(ps.StartOffset) + change)
   348  
   349  	newPs.LaidOutContent = make([]LaidOutContent, len(ps.LaidOutContent))
   350  	for idx, pc := range ps.LaidOutContent {
   351  		newPc := pc
   352  		newPc.StartOffset = quantity.Offset(int64(pc.StartOffset) + change)
   353  		newPs.LaidOutContent[idx] = newPc
   354  	}
   355  	return newPs
   356  }
   357  
   358  func isLayoutCompatible(current, new *PartiallyLaidOutVolume) error {
   359  	if current.ID != new.ID {
   360  		return fmt.Errorf("incompatible ID change from %v to %v", current.ID, new.ID)
   361  	}
   362  	if current.Schema != new.Schema {
   363  		return fmt.Errorf("incompatible schema change from %v to %v",
   364  			current.Schema, new.Schema)
   365  	}
   366  	if current.Bootloader != new.Bootloader {
   367  		return fmt.Errorf("incompatible bootloader change from %v to %v",
   368  			current.Bootloader, new.Bootloader)
   369  	}
   370  
   371  	// XXX: the code below asssumes both volumes have the same number of
   372  	// structures, this limitation may be lifter later
   373  	if len(current.LaidOutStructure) != len(new.LaidOutStructure) {
   374  		return fmt.Errorf("incompatible change in the number of structures from %v to %v",
   375  			len(current.LaidOutStructure), len(new.LaidOutStructure))
   376  	}
   377  
   378  	// at the structure level we expect the volume to be identical
   379  	for i := range current.LaidOutStructure {
   380  		from := &current.LaidOutStructure[i]
   381  		to := &new.LaidOutStructure[i]
   382  		if err := canUpdateStructure(from, to, new.Schema); err != nil {
   383  			return fmt.Errorf("incompatible structure %v change: %v", to, err)
   384  		}
   385  	}
   386  	return nil
   387  }