mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-19 10:46:58 +08:00
fix(glance): detect windows iso (#25341)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user