blob: f97ca8127d521b1a34631627f579bb62cc7fad1a [file]
// Copyright 2026 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
//
// 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 cosboot
import (
"bytes"
"fmt"
"log"
"os/exec"
"regexp"
"strconv"
"strings"
"cos.googlesource.com/cos/tools.git/src/pkg/tools/partutil"
)
var (
defaultARegex = regexp.MustCompile(`defaultA=(.*)`)
defaultBRegex = regexp.MustCompile(`defaultB=(.*)`)
menuEntryRegex = regexp.MustCompile(`(?m)^menuentry\s+.*\{[\s\S]*?^\}`)
)
func readGPTOnCOS() (partutil.SFDiskTable, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("rootdev", "-s", "-d")
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
log.Printf("Command %q failed with stderr: %v", cmd.String(), string(stderr.Bytes()))
return partutil.SFDiskTable{}, fmt.Errorf("cannot determine boot disk: %v", err)
}
disk := strings.TrimSpace(string(stdout.Bytes()))
return partutil.ReadPartitionTableJSON(disk)
}
func IsBootA(disk string) (bool, error) {
var gpt partutil.SFDiskTable
if disk == "" {
var err error
gpt, err = readGPTOnCOS()
if err != nil {
return false, err
}
} else {
var err error
gpt, err = partutil.ReadPartitionTableJSON(disk)
if err != nil {
return false, err
}
}
part2Attrs := gpt.PartitionTable.Partitions[1].Attrs
part2Prio, err := partutil.GPTPriority(part2Attrs)
if err != nil {
return false, fmt.Errorf("could not get partition 2 GPTPriority: %v", err)
}
part4Attrs := gpt.PartitionTable.Partitions[3].Attrs
part4Prio, err := partutil.GPTPriority(part4Attrs)
if err != nil {
return false, fmt.Errorf("could not get partition 4 GPTPriority: %v", err)
}
return part2Prio >= part4Prio, nil
}
func NextGRUBCmdLine(disk string) (string, error) {
grubConfig, err := ReadEFIFile(disk, partutil.GRUBCfgEFIPath)
if err != nil {
return "", err
}
grubConfigStr := string(grubConfig)
matches := defaultARegex.FindStringSubmatch(grubConfigStr)
if len(matches) < 2 {
return "", fmt.Errorf("could not find defaultA in grub config: %v", grubConfigStr)
}
defaultA, err := strconv.Atoi(matches[1])
if err != nil {
return "", err
}
matches = defaultBRegex.FindStringSubmatch(grubConfigStr)
if len(matches) < 2 {
return "", fmt.Errorf("could not find defaultB in grub config: %v", grubConfigStr)
}
defaultB, err := strconv.Atoi(matches[1])
if err != nil {
return "", err
}
useBootA, err := IsBootA(disk)
if err != nil {
return "", fmt.Errorf("Failed to determine if next boot is boot A on disk %q: %v", disk, err)
}
menuEntries := menuEntryRegex.FindAllString(grubConfigStr, -1)
var menuEntry string
if useBootA {
menuEntry = strings.TrimSpace(menuEntries[defaultA])
} else {
menuEntry = strings.TrimSpace(menuEntries[defaultB])
}
lines := strings.Split(menuEntry, "\n")
if len(lines) < 3 {
return "", fmt.Errorf("Menuentry is malformed: %v", menuEntry)
}
cmdline := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(lines[1]), "linux"))
return cmdline, nil
}
func EditGRUBConfig(disk, sedCmd string) error {
grubConfig, err := ReadEFIFile(disk, partutil.GRUBCfgEFIPath)
if err != nil {
return err
}
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("sed", "-e", sedCmd)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewBuffer(grubConfig)
if err := cmd.Run(); err != nil {
log.Printf("Command %q failed with stderr: %v", cmd.String(), string(stderr.Bytes()))
return fmt.Errorf("cannot sed grub config: %v", err)
}
updatedGrubConfig := stdout.Bytes()
return WriteEFIFile(disk, partutil.GRUBCfgEFIPath, updatedGrubConfig, 0644)
}
// ReplaceInSlotEntry replaces oldStr with newStr exclusively inside the menuentry
// corresponding to the specified slot (Slot A if isSlotB=false, Slot B if isSlotB=true).
func ReplaceInSlotEntry(grubContent string, isSlotB bool, oldStr, newStr string) string {
targetIdx := 0
if isSlotB {
targetIdx = 1
if m := defaultBRegex.FindStringSubmatch(grubContent); len(m) > 1 {
if idx, err := strconv.Atoi(m[1]); err == nil {
targetIdx = idx
}
}
} else {
if m := defaultARegex.FindStringSubmatch(grubContent); len(m) > 1 {
if idx, err := strconv.Atoi(m[1]); err == nil {
targetIdx = idx
}
}
}
entries := menuEntryRegex.FindAllString(grubContent, -1)
if targetIdx < 0 || targetIdx >= len(entries) {
log.Printf("Warning: slot index %d out of bounds (found %d entries)", targetIdx, len(entries))
return grubContent
}
targetEntry := entries[targetIdx]
replaceRe := regexp.MustCompile("(?i)" + regexp.QuoteMeta(oldStr))
patchedEntry := replaceRe.ReplaceAllString(targetEntry, newStr)
return strings.Replace(grubContent, targetEntry, patchedEntry, 1)
}