fix(glance): detect windows iso (#25341)

This commit is contained in:
屈轩
2026-08-14 19:01:47 +08:00
committed by GitHub
parent d072d5a110
commit f46730d262
12 changed files with 484 additions and 1724 deletions
+2 -2
View File
@@ -186,8 +186,8 @@ func DetectOSFromISO(r io.Reader) (*ISOInfo, error) {
return nil, err
}
// ========== 识别Windows系列 ==========
if reader.FileExists("sources/install.wim") {
// ========== 识别Windows系列install.wim 或 install.esd ==========
if reader.FileExists("sources/install.wim") || reader.FileExists("sources/install.esd") {
return DetectWindowsEdition(reader)
}
+211
View File
@@ -0,0 +1,211 @@
// Copyright 2019 Yunion
//
// 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 isoutils
import (
"encoding/binary"
"encoding/xml"
"fmt"
"io"
"unicode/utf16"
"yunion.io/x/log"
"yunion.io/x/pkg/util/imagetools"
)
var wimImageTag = [8]byte{'M', 'S', 'W', 'I', 'M', 0, 0, 0}
const (
wimResFlagCompressed = 0x04
)
// wimResourceDesc matches the on-disk WIM resource descriptor (24 bytes).
type wimResourceDesc struct {
FlagsAndCompressedSize uint64
Offset int64
OriginalSize int64
}
func (r wimResourceDesc) flags() byte {
return byte(r.FlagsAndCompressedSize >> 56)
}
func (r wimResourceDesc) compressedSize() int64 {
return int64(r.FlagsAndCompressedSize & 0xffffffffffffff)
}
// wimHeaderDisk is the on-disk WIM header (208 bytes / 0xd0).
type wimHeaderDisk struct {
ImageTag [8]byte
Size uint32
Version uint32
Flags uint32
CompressionSize uint32
WIMGuid [16]byte
PartNumber uint16
TotalParts uint16
ImageCount uint32
OffsetTable wimResourceDesc
XMLData wimResourceDesc
BootMetadata wimResourceDesc
BootIndex uint32
Padding uint32
Integrity wimResourceDesc
Unused [60]byte
}
type wimXMLInfo struct {
Image []wimXMLImage `xml:"IMAGE"`
}
type wimXMLImage struct {
Name string `xml:"NAME"`
Index int `xml:"INDEX,attr"`
Windows *wimWindowsInfo `xml:"WINDOWS"`
}
type wimWindowsInfo struct {
Arch byte `xml:"ARCH"`
ProductName string `xml:"PRODUCTNAME"`
EditionID string `xml:"EDITIONID"`
ProductType string `xml:"PRODUCTTYPE"`
DefaultLanguage string `xml:"LANGUAGES>DEFAULT"`
Version wimXMLVersion `xml:"VERSION"`
}
type wimXMLVersion struct {
Major int `xml:"MAJOR"`
Minor int `xml:"MINOR"`
Build int `xml:"BUILD"`
}
// parseWimXmlMetadata reads WIM/ESD header + uncompressed XML metadata and maps Windows version.
// Content compression (LZMS/XPRESS) is ignored; only the XML blob is required for edition detection.
func parseWimXmlMetadata(r io.ReaderAt) (*ISOInfo, error) {
var hdr wimHeaderDisk
if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(hdr))), binary.LittleEndian, &hdr); err != nil {
return nil, fmt.Errorf("read WIM header: %w", err)
}
if hdr.ImageTag != wimImageTag {
return nil, fmt.Errorf("not a WIM/ESD file")
}
if hdr.XMLData.compressedSize() == 0 || hdr.XMLData.OriginalSize == 0 {
return nil, fmt.Errorf("WIM/ESD has no XML metadata")
}
if hdr.XMLData.flags()&wimResFlagCompressed != 0 {
return nil, fmt.Errorf("compressed WIM XML metadata is not supported")
}
xmlBytes := make([]byte, hdr.XMLData.OriginalSize)
if _, err := r.ReadAt(xmlBytes, hdr.XMLData.Offset); err != nil {
return nil, fmt.Errorf("read WIM XML metadata: %w", err)
}
xmlStr, err := decodeWimUTF16XML(xmlBytes)
if err != nil {
return nil, err
}
var info wimXMLInfo
if err := xml.Unmarshal([]byte(xmlStr), &info); err != nil {
return nil, fmt.Errorf("parse WIM XML: %w", err)
}
for _, image := range info.Image {
if image.Windows == nil {
continue
}
result := mapWindowsVersion(image.Windows)
ver := fmt.Sprintf("%d.%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor, image.Windows.Version.Build)
log.Debugf("识别到 %s 版本: %s -> %s", result.Distro, ver, result.Version)
return result, nil
}
return nil, fmt.Errorf("no WINDOWS metadata found in WIM/ESD XML")
}
func decodeWimUTF16XML(data []byte) (string, error) {
if len(data) < 2 || len(data)%2 != 0 {
return "", fmt.Errorf("invalid WIM XML encoding")
}
u16 := make([]uint16, len(data)/2)
for i := 0; i < len(u16); i++ {
u16[i] = binary.LittleEndian.Uint16(data[i*2:])
}
// BOM is little-endian UTF-16 (0xFEFF)
if u16[0] != 0xfeff {
return "", fmt.Errorf("invalid WIM XML BOM")
}
return string(utf16.Decode(u16[1:])), nil
}
func mapWindowsVersion(win *wimWindowsInfo) *ISOInfo {
result := &ISOInfo{
Distro: imagetools.OS_DIST_WINDOWS,
Language: win.DefaultLanguage,
}
switch win.Arch {
case 9:
result.Arch = "x86_64"
case 12:
result.Arch = "arm64"
case 0:
result.Arch = "x86"
}
majMin := fmt.Sprintf("%d.%d", win.Version.Major, win.Version.Minor)
switch majMin {
case "6.0":
result.Version = "Windows Vista"
case "6.1":
result.Version = "Windows 7"
case "6.2":
result.Version = "Windows 8"
case "6.3":
result.Version = "Windows 8.1"
case "10.0":
if win.Version.Build >= 27500 {
result.Version = "Windows 12"
} else if win.Version.Build >= 22000 {
result.Version = "Windows 11"
} else {
result.Version = "Windows 10"
}
}
if win.ProductType == "ServerNT" {
result.Distro = imagetools.OS_DIST_WINDOWS_SERVER
switch majMin {
case "6.0":
result.Version = "Windows Server 2008"
case "6.1":
result.Version = "Windows Server 2008 R2"
case "6.2":
result.Version = "Windows Server 2012"
case "6.3":
result.Version = "Windows Server 2012 R2"
case "10.0":
if win.Version.Build >= 26040 {
result.Version = "Windows Server 2025"
} else if win.Version.Build >= 20348 {
result.Version = "Windows Server 2022"
} else if win.Version.Build >= 17763 {
result.Version = "Windows Server 2019"
} else if win.Version.Build >= 14393 {
result.Version = "Windows Server 2016"
}
}
}
return result
}
+197
View File
@@ -0,0 +1,197 @@
// Copyright 2019 Yunion
//
// 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 isoutils
import (
"bytes"
"encoding/binary"
"testing"
"unicode/utf16"
"yunion.io/x/pkg/util/imagetools"
)
func encodeWimUTF16XML(s string) []byte {
u16 := utf16.Encode([]rune(s))
out := make([]byte, 2+len(u16)*2)
binary.LittleEndian.PutUint16(out[0:], 0xfeff) // BOM
for i, v := range u16 {
binary.LittleEndian.PutUint16(out[2+i*2:], v)
}
return out
}
func buildMinimalWimWithXML(xmlASCII string) []byte {
xmlData := encodeWimUTF16XML(xmlASCII)
hdrSize := binary.Size(wimHeaderDisk{})
offset := int64(hdrSize)
var hdr wimHeaderDisk
hdr.ImageTag = wimImageTag
hdr.Size = uint32(hdrSize)
hdr.Version = 0x10d00
hdr.PartNumber = 1
hdr.TotalParts = 1
hdr.ImageCount = 1
// uncompressed XML resource: flags=0, compressed size = original size
hdr.XMLData = wimResourceDesc{
FlagsAndCompressedSize: uint64(len(xmlData)),
Offset: offset,
OriginalSize: int64(len(xmlData)),
}
buf := &bytes.Buffer{}
_ = binary.Write(buf, binary.LittleEndian, &hdr)
buf.Write(xmlData)
return buf.Bytes()
}
func TestParseWimXmlMetadataWindows11(t *testing.T) {
xml := `<?xml version="1.0"?>
<WIM>
<IMAGE INDEX="1">
<NAME>Windows 11 Pro</NAME>
<WINDOWS>
<ARCH>9</ARCH>
<PRODUCTNAME>Microsoft® Windows® Operating System</PRODUCTNAME>
<EDITIONID>Professional</EDITIONID>
<PRODUCTTYPE>WinNT</PRODUCTTYPE>
<LANGUAGES>
<LANGUAGE>zh-CN</LANGUAGE>
<DEFAULT>zh-CN</DEFAULT>
</LANGUAGES>
<VERSION>
<MAJOR>10</MAJOR>
<MINOR>0</MINOR>
<BUILD>22631</BUILD>
</VERSION>
</WINDOWS>
</IMAGE>
</WIM>`
data := buildMinimalWimWithXML(xml)
info, err := parseWimXmlMetadata(bytes.NewReader(data))
if err != nil {
t.Fatalf("parseWimXmlMetadata: %v", err)
}
if info.Distro != imagetools.OS_DIST_WINDOWS {
t.Fatalf("distro: got %s want %s", info.Distro, imagetools.OS_DIST_WINDOWS)
}
if info.Version != "Windows 11" {
t.Fatalf("version: got %s want Windows 11", info.Version)
}
if info.Arch != "x86_64" {
t.Fatalf("arch: got %s want x86_64", info.Arch)
}
if info.Language != "zh-CN" {
t.Fatalf("language: got %s want zh-CN", info.Language)
}
}
func TestParseWimXmlMetadataWindowsServer2022(t *testing.T) {
xml := `<?xml version="1.0"?>
<WIM>
<IMAGE INDEX="1">
<NAME>Windows Server 2022 SERVERSTANDARD</NAME>
<WINDOWS>
<ARCH>9</ARCH>
<PRODUCTTYPE>ServerNT</PRODUCTTYPE>
<LANGUAGES>
<DEFAULT>en-US</DEFAULT>
</LANGUAGES>
<VERSION>
<MAJOR>10</MAJOR>
<MINOR>0</MINOR>
<BUILD>20348</BUILD>
</VERSION>
</WINDOWS>
</IMAGE>
</WIM>`
data := buildMinimalWimWithXML(xml)
info, err := parseWimXmlMetadata(bytes.NewReader(data))
if err != nil {
t.Fatalf("parseWimXmlMetadata: %v", err)
}
if info.Distro != imagetools.OS_DIST_WINDOWS_SERVER {
t.Fatalf("distro: got %s want %s", info.Distro, imagetools.OS_DIST_WINDOWS_SERVER)
}
if info.Version != "Windows Server 2022" {
t.Fatalf("version: got %s want Windows Server 2022", info.Version)
}
}
func TestParseWimXmlMetadataRejectsCompressedXML(t *testing.T) {
xmlData := encodeWimUTF16XML(`<?xml version="1.0"?><WIM></WIM>`)
hdrSize := binary.Size(wimHeaderDisk{})
var hdr wimHeaderDisk
hdr.ImageTag = wimImageTag
hdr.Size = uint32(hdrSize)
hdr.PartNumber = 1
hdr.TotalParts = 1
hdr.XMLData = wimResourceDesc{
FlagsAndCompressedSize: uint64(wimResFlagCompressed)<<56 | uint64(len(xmlData)),
Offset: int64(hdrSize),
OriginalSize: int64(len(xmlData)),
}
buf := &bytes.Buffer{}
_ = binary.Write(buf, binary.LittleEndian, &hdr)
buf.Write(xmlData)
_, err := parseWimXmlMetadata(bytes.NewReader(buf.Bytes()))
if err == nil {
t.Fatal("expected error for compressed XML")
}
}
func TestMapWindowsVersion(t *testing.T) {
cases := []struct {
name string
win wimWindowsInfo
distro string
version string
}{
{
name: "win10",
win: wimWindowsInfo{
Arch: 9, ProductType: "WinNT",
Version: wimXMLVersion{Major: 10, Minor: 0, Build: 19045},
},
distro: imagetools.OS_DIST_WINDOWS, version: "Windows 10",
},
{
name: "win7",
win: wimWindowsInfo{
Arch: 9, ProductType: "WinNT",
Version: wimXMLVersion{Major: 6, Minor: 1, Build: 7601},
},
distro: imagetools.OS_DIST_WINDOWS, version: "Windows 7",
},
{
name: "server2019",
win: wimWindowsInfo{
Arch: 9, ProductType: "ServerNT",
Version: wimXMLVersion{Major: 10, Minor: 0, Build: 17763},
},
distro: imagetools.OS_DIST_WINDOWS_SERVER, version: "Windows Server 2019",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
info := mapWindowsVersion(&c.win)
if info.Distro != c.distro || info.Version != c.version {
t.Fatalf("got %s/%s want %s/%s", info.Distro, info.Version, c.distro, c.version)
}
})
}
}
+21 -71
View File
@@ -19,80 +19,30 @@ package isoutils
import (
"fmt"
"github.com/Microsoft/go-winio/wim"
"yunion.io/x/log"
"yunion.io/x/pkg/util/imagetools"
)
// ========== 7. 保留Windows版本识别函数(适配新结构) ==========
// DetectWindowsEdition reads sources/install.wim or sources/install.esd XML metadata
// to determine Windows edition/version. Prefer .wim, fall back to .esd.
func DetectWindowsEdition(r *ISOFileReader) (*ISOInfo, error) {
wimFile, err := r.GetFile("sources/install.wim")
if err != nil {
return nil, err
}
wim, err := wim.NewReader(wimFile.NewReader())
if err != nil {
return nil, err
}
result := &ISOInfo{}
for _, image := range wim.Image {
version := fmt.Sprintf("%d.%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor, image.Windows.Version.Build)
if image.Windows != nil {
if image.Windows.Arch == 9 {
result.Arch = "x86_64"
} else if image.Windows.Arch == 12 {
result.Arch = "arm64"
} else if image.Windows.Arch == 0 {
result.Arch = "x86"
}
result.Distro = imagetools.OS_DIST_WINDOWS
result.Language = image.Windows.DefaultLanguage
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
case "6.0":
result.Version = "Windows Vista"
case "6.1":
result.Version = "Windows 7"
case "6.2":
result.Version = "Windows 8"
case "6.3":
result.Version = "Windows 8.1"
case "10.0":
if image.Windows.Version.Build >= 27500 {
result.Version = "Windows 12"
} else if image.Windows.Version.Build >= 22000 {
result.Version = "Windows 11"
} else {
result.Version = "Windows 10"
}
}
if image.Windows.ProductType == "ServerNT" {
result.Distro = imagetools.OS_DIST_WINDOWS_SERVER
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
case "6.0":
result.Version = "Windows Server 2008"
case "6.1":
result.Version = "Windows Server 2008 R2"
case "6.2":
result.Version = "Windows Server 2012"
case "6.3":
result.Version = "Windows Server 2012 R2"
case "10.0":
if image.Windows.Version.Build >= 26040 {
result.Version = "Windows Server 2025"
} else if image.Windows.Version.Build >= 20348 {
result.Version = "Windows Server 2022"
} else if image.Windows.Version.Build >= 17763 {
result.Version = "Windows Server 2019"
} else if image.Windows.Version.Build >= 14393 {
result.Version = "Windows Server 2016"
}
}
}
log.Debugf("识别到 %s 版本: %s -> %s", result.Distro, version, result.Version)
break
var lastErr error
for _, path := range []string{"sources/install.wim", "sources/install.esd"} {
if !r.FileExists(path) {
continue
}
f, err := r.GetFile(path)
if err != nil {
lastErr = fmt.Errorf("open %s: %w", path, err)
continue
}
info, err := parseWimXmlMetadata(f.NewReader())
if err != nil {
lastErr = fmt.Errorf("parse %s: %w", path, err)
continue
}
return info, nil
}
return result, nil
if lastErr != nil {
return nil, lastErr
}
return nil, fmt.Errorf("sources/install.wim or sources/install.esd not found")
}