blob: 5c8c3d25eec7524a0f28b90fb45df07daf2a29a3 [file] [log] [blame]
// 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
}